drafted 1.19.7 → 1.19.8
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 +99 -7
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -384,11 +384,11 @@ const TOOL_ANNOTATIONS = {
|
|
|
384
384
|
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.' },
|
|
385
385
|
|
|
386
386
|
// Identity — read-only introspection of THIS agent's session
|
|
387
|
-
whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
|
|
387
|
+
whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 (`authState` says whether this session is signed in, and `authNote` names the exact call that fixes it when it is not — never diagnose a null userId yourself) — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
|
|
388
388
|
|
|
389
389
|
// Session naming — the name-before-work gate: every agent session must set a short
|
|
390
390
|
// name describing the work before any other tool call succeeds.
|
|
391
|
-
session: { title: 'Session', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Name THIS agent session (and rename it later). The name is what the user sees on your surface tab — pick a short 2-3 word description of the work (e.g. "beoflow backend", "drafted fs work"). The name persists across reconnects and restarts; you only set it once unless the work changes. Dispatch by `action`: `name` (set/rename with the `name` param).' },
|
|
391
|
+
session: { title: 'Session', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Name THIS agent session (and rename it later). The name is what the user sees on your surface tab — pick a short 2-3 word description of the work (e.g. "beoflow backend", "drafted fs work"). The name persists across reconnects and restarts; you only set it once unless the work changes. Dispatch by `action`: `name` (set/rename with the `name` param) or `update` (update the npm-installed Drafted MCP on THIS machine when whoami reports mcpUpdate.stale — run it yourself rather than handing the user a shell command; `dryRun` reports without starting it).' },
|
|
392
392
|
|
|
393
393
|
// Comments — the review loop agents could previously only reach over raw HTTP
|
|
394
394
|
comment: { title: 'Comments', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Read and write review comments on frames. Comments are notes ABOUT THE WORK: they attach to a frame, optionally to one element inside it, and anyone who can see the frame sees them. Dispatch by `action`: list (paginated, compact mode), add (with an optional element anchor or a reply), resolve, reopen, delete. Use this to leave findings a human can action in place, and to read the feedback they left you.' },
|
|
@@ -2204,6 +2204,8 @@ async function sessionSurfaceBlock() {
|
|
|
2204
2204
|
if (agentSurface) {
|
|
2205
2205
|
return {
|
|
2206
2206
|
sessionId,
|
|
2207
|
+
authenticated: !!agentSurface.userId,
|
|
2208
|
+
authState: agentSurface.userId ? 'authenticated' : 'signed_out',
|
|
2207
2209
|
userId: agentSurface.userId ?? null,
|
|
2208
2210
|
orgId: agentSurface.orgId ?? null,
|
|
2209
2211
|
projectId: agentSurface.projectId ?? null,
|
|
@@ -2232,6 +2234,7 @@ async function sessionSurfaceBlock() {
|
|
|
2232
2234
|
try {
|
|
2233
2235
|
const res = await serverFetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
|
|
2234
2236
|
if (res.ok) me = await res.json();
|
|
2237
|
+
// else: the server ANSWERED and rejected this session — a real sign-out, not a blip.
|
|
2235
2238
|
} catch (e) {
|
|
2236
2239
|
// Transport failure, NOT a sign-out. Conflating the two made agents tell
|
|
2237
2240
|
// users "you're signed out" during a network blip and start needless
|
|
@@ -2242,8 +2245,26 @@ async function sessionSurfaceBlock() {
|
|
|
2242
2245
|
}
|
|
2243
2246
|
}
|
|
2244
2247
|
const nameRequired = !!me?.agentClone && !me?.surfaceName;
|
|
2248
|
+
// A null userId used to come back BARE — no reason, no next step — and agents filled the
|
|
2249
|
+
// gap by inventing one: a real report has an agent telling the user to look for a pending
|
|
2250
|
+
// "device/session approval prompt" in the desktop app (no such prompt exists) and then
|
|
2251
|
+
// waiting on it. Name which of the four states this is and the exact call that ends it.
|
|
2252
|
+
const pending = (!me && !unreachable) ? getPendingDeviceCode() : null;
|
|
2253
|
+
const authState = me?.userId ? 'authenticated'
|
|
2254
|
+
: unreachable ? 'server_unreachable'
|
|
2255
|
+
: pending ? 'awaiting_approval'
|
|
2256
|
+
: cookieSid ? 'session_rejected'
|
|
2257
|
+
: 'signed_out';
|
|
2258
|
+
const AUTH_NOTES = {
|
|
2259
|
+
awaiting_approval: `Sign-in started but NOT yet approved. Give the user this link and ask them to open it: ${pending?.verificationUrl || '(run auth(action="get_link") for a fresh link)'} — this session authenticates itself once they approve; nothing to approve inside the desktop app.`,
|
|
2260
|
+
session_rejected: 'Not signed in: this machine has a stored Drafted session but the server rejected it (expired or revoked). Call auth(action="get_link") and give the user the returned URL. Do NOT ask them to look for an approval prompt in the desktop app — being signed in to the app does not sign THIS agent in.',
|
|
2261
|
+
signed_out: 'Not signed in: no Drafted session on this machine. Call auth(action="get_link") and give the user the returned URL. Do NOT ask them to look for an approval prompt in the desktop app — being signed in to the app does not sign THIS agent in.',
|
|
2262
|
+
};
|
|
2245
2263
|
return {
|
|
2246
2264
|
sessionId: cookieSid,
|
|
2265
|
+
authenticated: !!me?.userId,
|
|
2266
|
+
authState,
|
|
2267
|
+
...(AUTH_NOTES[authState] ? { authNote: AUTH_NOTES[authState] } : {}),
|
|
2247
2268
|
userId: me?.userId ?? null,
|
|
2248
2269
|
orgId: me?.currentOrg?.id ?? null,
|
|
2249
2270
|
projectId: getState().projectId ?? null,
|
|
@@ -2254,6 +2275,9 @@ async function sessionSurfaceBlock() {
|
|
|
2254
2275
|
color: null,
|
|
2255
2276
|
surfaced: false,
|
|
2256
2277
|
alive: false,
|
|
2278
|
+
// `alive` is bare here where it isn't in the acked branch, and a bare false reads as
|
|
2279
|
+
// "something is broken" — it only means the surface WebSocket hasn't acked yet.
|
|
2280
|
+
aliveMeaning: 'The surface WebSocket has not acked this session yet, so no canvas is known to be open. Frame, wiki, and skill writes all persist normally — only focus/presence have nothing to draw on. Do not change what you write because of this, and do not report it as an error.',
|
|
2257
2281
|
...(unreachable ? {
|
|
2258
2282
|
serverUnreachable: true,
|
|
2259
2283
|
note: unreachableWhy || `Could not reach ${getServerUrl()} — identity UNKNOWN, not signed out. This is a network/transport failure: do not tell the user they are logged out and do not start a new login; retry when connectivity is back.`,
|
|
@@ -2285,7 +2309,7 @@ async function getGoogleDriveAvailability() {
|
|
|
2285
2309
|
// reachability, and installed-MCP staleness in ONE bootstrap call. The update data is
|
|
2286
2310
|
// cached (5min), so repeat `whoami` calls are free; the server-side update gate still
|
|
2287
2311
|
// blocks mutating calls on its own, independent of this tool. Read-only — no state changed.
|
|
2288
|
-
tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only.', {}, async () => {
|
|
2312
|
+
tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 (`authState` says whether this session is signed in, and `authNote` names the exact call that fixes it when it is not — never diagnose a null userId yourself) — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only.', {}, async () => {
|
|
2289
2313
|
try {
|
|
2290
2314
|
// Ensure the child clone exists BEFORE reading identity — otherwise the /auth/me
|
|
2291
2315
|
// fallback (pre-WS-ack) queries the ROOT session and reports the wrong naming state.
|
|
@@ -2295,7 +2319,19 @@ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human
|
|
|
2295
2319
|
// Tell the agent to actually surface its name to the user — returning `name` in the JSON isn't
|
|
2296
2320
|
// enough; without an explicit instruction agents rarely say which session they are, so users
|
|
2297
2321
|
// can't match them to their tab on the Drafted surface.
|
|
2298
|
-
|
|
2322
|
+
// Auth outranks naming: an unauthenticated session has nothing to name, and leaving the
|
|
2323
|
+
// instruction empty is what let agents invent their own explanation for the null ids.
|
|
2324
|
+
// A stale install can now fix ITSELF — say so here, since this is where staleness is
|
|
2325
|
+
// reported and an agent that only reads `stale: true` goes back to the user with a shell
|
|
2326
|
+
// command they didn't ask for.
|
|
2327
|
+
const staleNote = mcpUpdate.stale && mcpUpdate.mode === 'stdio'
|
|
2328
|
+
? ` This machine's installed Drafted MCP is out of date (${mcpUpdate.currentVersion} → ${mcpUpdate.latestVersion || 'latest'})${mcpUpdate.required ? ' and BELOW the minimum required version, so mutating tools are blocked' : ''}. Call session(action="update") to update it yourself — do not hand the user a shell command.`
|
|
2329
|
+
: '';
|
|
2330
|
+
const instruction = block.authNote
|
|
2331
|
+
? block.authNote + staleNote
|
|
2332
|
+
: staleNote && !block.nameRequired
|
|
2333
|
+
? (block.name ? `You are the session named "${block.name}". Tell the user you're "${block.name}" in your reply so they can match you to your tab on the Drafted surface.` : '') + staleNote
|
|
2334
|
+
: block.nameRequired
|
|
2299
2335
|
? `You have NO session name yet — the name-before-work gate blocks all other tools until you set one. Name this session by the work you're doing with the user: a short 2-3 word description (suggested: "${block.suggestedName || '...'}"). Set it now with session(action="name", name="<your name>"), then tell the user you're "<name>".`
|
|
2300
2336
|
: block.name
|
|
2301
2337
|
? `You are the session named "${block.name}". Tell the user you're "${block.name}" in your reply so they can match you to your tab on the Drafted surface.`
|
|
@@ -2314,13 +2350,69 @@ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human
|
|
|
2314
2350
|
} catch (error) { return err(error); }
|
|
2315
2351
|
});
|
|
2316
2352
|
|
|
2353
|
+
// Self-update: run the installed CLI's own updater so THIS machine's stale stdio MCP can be
|
|
2354
|
+
// refreshed from inside a tool call. Before this, the only path was telling the user to run a
|
|
2355
|
+
// shell command — which a web/Cowork agent cannot do at all, and which a stale install needs
|
|
2356
|
+
// most (an out-of-date MCP is exactly the session least able to ask for help). The updater
|
|
2357
|
+
// mechanics live in ONE place, `drafted update --yes` (cli/drafted.mjs) — do not reimplement
|
|
2358
|
+
// the installer command here, that's the two-writers drift.
|
|
2359
|
+
//
|
|
2360
|
+
// The `drafted` bin is resolved at the FIXED prefix first: the stdio MCP resolves from
|
|
2361
|
+
// ~/.drafted/npm-global, which a non-login shell's PATH does not carry, so bare `drafted`
|
|
2362
|
+
// finds nothing (or worse, a different install) on many machines.
|
|
2363
|
+
function installedDraftedBin() {
|
|
2364
|
+
const prefixed = platform() === 'win32'
|
|
2365
|
+
? join(homedir(), '.drafted', 'npm-global', 'drafted.cmd')
|
|
2366
|
+
: join(homedir(), '.drafted', 'npm-global', 'bin', 'drafted');
|
|
2367
|
+
return existsSync(prefixed) ? prefixed : 'drafted';
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
async function updateInstalledMcp({ dryRun = false } = {}) {
|
|
2371
|
+
try {
|
|
2372
|
+
const metadata = await getMcpUpdateMetadata();
|
|
2373
|
+
const instructions = buildInstalledMcpUpdateInstructions(metadata);
|
|
2374
|
+
// Hosted HTTP MCP has no local daemon — instructions already say so.
|
|
2375
|
+
if (!instructions.updateSupported) return ok({ ...instructions, action: 'update' });
|
|
2376
|
+
if (dryRun) return ok({ ...instructions, action: 'update', started: false, dryRun: true });
|
|
2377
|
+
|
|
2378
|
+
const bin = installedDraftedBin();
|
|
2379
|
+
const result = await new Promise((resolve) => {
|
|
2380
|
+
execFile(bin, ['update', '--yes', '--json'], { timeout: 60_000 }, (error, stdout) => {
|
|
2381
|
+
if (error) return resolve({ ok: false, error: error.code === 'ENOENT' ? `Drafted CLI not found (looked for ${bin})` : (error.killed ? 'Updater did not start within 60s' : error.message) });
|
|
2382
|
+
try { resolve({ ok: true, data: JSON.parse(stdout) }); }
|
|
2383
|
+
catch { resolve({ ok: true, data: null }); }
|
|
2384
|
+
});
|
|
2385
|
+
});
|
|
2386
|
+
|
|
2387
|
+
if (!result.ok) {
|
|
2388
|
+
return ok({
|
|
2389
|
+
...instructions,
|
|
2390
|
+
action: 'update',
|
|
2391
|
+
started: false,
|
|
2392
|
+
error: result.error,
|
|
2393
|
+
note: `Could not start the updater from this process (${result.error}). Give the user this command to run in a terminal instead: ${instructions.manualCommand}`,
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
return ok({
|
|
2397
|
+
...instructions,
|
|
2398
|
+
action: 'update',
|
|
2399
|
+
started: true,
|
|
2400
|
+
// Be exact about what did and did not happen: the installer runs DETACHED and does not
|
|
2401
|
+
// replace the already-loaded process, so "updated" would be a lie until a restart.
|
|
2402
|
+
note: 'The updater is running in the background (~30-60s). It does NOT replace this already-running MCP process: tell the user to restart their agent/editor once it finishes, then call whoami to confirm the new mcpVersion.',
|
|
2403
|
+
});
|
|
2404
|
+
} catch (error) { return err(error); }
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2317
2407
|
// Session naming: set/rename THIS agent session's name — the name-before-work gate stays
|
|
2318
2408
|
// closed until this succeeds. The name persists server-side (sessions.surface_name), so
|
|
2319
2409
|
// reconnects and server restarts keep it and the gate never re-fires for a named session.
|
|
2320
|
-
tool('session', '
|
|
2321
|
-
action: z.enum(['name']).describe('Operation
|
|
2410
|
+
tool('session', 'Manage THIS agent session. `action="name"` sets/renames it — the name is what the user sees on your surface tab, so pick a short 2-3 word description of the work (e.g. "beoflow backend"); it persists across reconnects and restarts. `action="update"` updates the npm-installed Drafted MCP on THIS machine when whoami reports mcpUpdate.stale — run it yourself instead of asking the user to run a shell command.', {
|
|
2411
|
+
action: z.enum(['name', 'update']).describe('Operation: `name` (set/rename this session) or `update` (update the installed Drafted MCP on this machine).'),
|
|
2322
2412
|
name: z.string().optional().describe('[name] the session name — a short 2-3 word description of the work (e.g. "beoflow backend"). Max 5 words / 50 chars.'),
|
|
2323
|
-
|
|
2413
|
+
dryRun: z.boolean().optional().describe('[update] report what the update would do without starting it.'),
|
|
2414
|
+
}, async ({ action, name, dryRun }) => {
|
|
2415
|
+
if (action === 'update') return updateInstalledMcp({ dryRun });
|
|
2324
2416
|
if (action !== 'name') return err(new Error(`unknown session action: ${action}`));
|
|
2325
2417
|
if (!name || !String(name).trim()) return err(new Error('name required — a short 2-3 word description of the work, e.g. session(action="name", name="beoflow backend")'));
|
|
2326
2418
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.8",
|
|
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": [
|