drafted 1.14.1 → 1.14.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.
Files changed (2) hide show
  1. package/mcp/server.mjs +93 -24
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -225,6 +225,9 @@ const TOOL_ANNOTATIONS = {
225
225
  // Identity — read-only introspection of THIS agent's session
226
226
  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. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
227
227
 
228
+ // Health — call once per session, right after whoami and before any real work
229
+ health: { title: 'Server health', readOnlyHint: true, destructiveHint: false, openWorldHint: true, description: 'Server reachability + installed MCP version/update status. Call this once per session, right after whoami and before doing any real work, so a required update surfaces before you act on stale tool behavior.' },
230
+
228
231
  // Projects
229
232
  project: { title: 'Projects', readOnlyHint: false, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html', description: 'Manage projects: list (start here), open (bind this agent session to a project — org derives from it), create (org= names where it is born), update, move to another org.' },
230
233
  get_org: { title: 'Organization', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'List your orgs, the default org, and Google Drive availability (action="get", default), or fetch installed MCP update instructions (action="update_mcp"). There is no org switching — org derives from the resource you address; creates/searches take org=. When googleDrive.connected is true, strongly prefer Google Workspace frames for documents, sheets, and slides.' },
@@ -371,6 +374,10 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
371
374
  state.currentTool = name;
372
375
  trackUmamiEvent(UMAMI_EVENTS.MCP_TOOL_CALLED, { tool: name, projectId: state.projectId || undefined, source: 'mcp' });
373
376
  reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_REQUEST, { tool: name });
377
+ if (!hasAnnouncedSubstantiveWork && !NON_SUBSTANTIVE_TOOLS.has(name)) {
378
+ hasAnnouncedSubstantiveWork = true;
379
+ announceSubstantiveWork().catch(() => {});
380
+ }
374
381
  try {
375
382
  const requiredUpdateError = await getRequiredMcpUpdateError(name, args?.[0] || {});
376
383
  if (requiredUpdateError) return err(new Error(requiredUpdateError));
@@ -1140,15 +1147,27 @@ function getCurrentProjectContext() {
1140
1147
  return s.projectMeta || { id: s.projectId, slug: null, name: null, orgId: null };
1141
1148
  }
1142
1149
 
1150
+ function scheduleAgentWsRetry() {
1151
+ clearTimeout(agentWsReconnectTimer);
1152
+ agentWsReconnectTimer = setTimeout(() => {
1153
+ connectAgentWs().catch((e) => {
1154
+ console.error('[MCP-WS] Reconnect failed:', e?.message || e);
1155
+ });
1156
+ }, 5000);
1157
+ }
1158
+
1143
1159
  async function connectAgentWs() {
1144
1160
  await ensureSession();
1145
1161
  const auth = getAuthHeaders();
1146
- if (!auth.Cookie) return;
1162
+ // No session yet (e.g. desktop sign-in still in progress) — retry instead of stranding
1163
+ // the handshake forever, since there's no WebSocket here yet to trigger the close-based
1164
+ // reconnect below.
1165
+ if (!auth.Cookie) { scheduleAgentWsRetry(); return; }
1147
1166
 
1148
1167
  const serverUrl = getServerUrl().replace(/^http/, 'ws');
1149
1168
  try {
1150
1169
  agentWs = new WebSocket(serverUrl, { headers: auth });
1151
- } catch { return; }
1170
+ } catch { scheduleAgentWsRetry(); return; }
1152
1171
 
1153
1172
  agentWs.on('open', () => {
1154
1173
  console.error('[MCP-WS] Connected');
@@ -1182,12 +1201,7 @@ async function connectAgentWs() {
1182
1201
  // and survive server restarts, so reusing the clone keeps this agent's surface (and
1183
1202
  // its playful tab name) stable across blips — otherwise every reconnect re-clones a
1184
1203
  // fresh session → a NEW greyed tab with a NEW name. A real 401 still triggers re-clone.
1185
- clearTimeout(agentWsReconnectTimer);
1186
- agentWsReconnectTimer = setTimeout(() => {
1187
- connectAgentWs().catch((e) => {
1188
- console.error('[MCP-WS] Reconnect failed:', e?.message || e);
1189
- });
1190
- }, 5000);
1204
+ scheduleAgentWsRetry();
1191
1205
  });
1192
1206
 
1193
1207
  agentWs.on('error', () => {
@@ -1223,6 +1237,28 @@ async function joinAgentWsRoom(projectId) {
1223
1237
  }
1224
1238
  }
1225
1239
 
1240
+ // Tools that are pure introspection/sign-in, not "the agent started working" — excluded from
1241
+ // the first-substantive-action ping below so a bare whoami/health/auth never yanks the
1242
+ // desktop app's window to the front.
1243
+ const NON_SUBSTANTIVE_TOOLS = new Set(['whoami', 'health', 'auth']);
1244
+ let hasAnnouncedSubstantiveWork = false;
1245
+
1246
+ // Tell the server this agent has started real work (first call past whoami/health/auth this
1247
+ // process), so it can foreground the desktop app's window. Fire-and-forget — never blocks or
1248
+ // fails the tool call that triggered it.
1249
+ async function announceSubstantiveWork() {
1250
+ if (!agentWs || agentWs.readyState > WebSocket.OPEN) {
1251
+ await connectAgentWs();
1252
+ }
1253
+ if (!agentWs) return;
1254
+ const msg = JSON.stringify({ type: 'agent-active', projectId: getState().projectId || null });
1255
+ if (agentWs.readyState === WebSocket.OPEN) {
1256
+ agentWs.send(msg);
1257
+ } else if (agentWs.readyState === WebSocket.CONNECTING) {
1258
+ agentWs.once('open', () => agentWs.send(msg));
1259
+ }
1260
+ }
1261
+
1226
1262
  // Clone session and connect WebSocket on startup (delayed to let server be ready).
1227
1263
  // Guarded because createMcpServer() runs per HTTP request — the bootstrap must
1228
1264
  // fire exactly once per process, not per request.
@@ -1684,11 +1720,35 @@ async function sessionSurfaceBlock() {
1684
1720
  // Identity: report THIS agent session's own surface identity. Read-only — no state changed.
1685
1721
  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. Read-only.', {}, async () => {
1686
1722
  try {
1723
+ const block = await sessionSurfaceBlock();
1724
+ // Tell the agent to actually surface its name to the user — returning `name` in the JSON isn't
1725
+ // enough; without an explicit instruction agents rarely say which session they are, so users
1726
+ // can't match them to their tab on the Drafted surface.
1727
+ const instruction = block.name
1728
+ ? `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.`
1729
+ : undefined;
1687
1730
  return ok({
1688
1731
  server: getServerUrl(),
1689
1732
  editor: (process.env.DRAFTED_AGENT_NAME || '').trim() || null,
1690
1733
  agentLabel: getAgentLabel(),
1691
- ...(await sessionSurfaceBlock()),
1734
+ ...block,
1735
+ ...(instruction ? { instruction } : {}),
1736
+ });
1737
+ } catch (error) { return err(error); }
1738
+ });
1739
+
1740
+ // Health: server reachability + installed-MCP staleness, meant to be the second call of a
1741
+ // session (right after whoami, before real work) so a required update surfaces early instead
1742
+ // of depending on an agent remembering to call get_org. Cached per-process — `whoami` stays
1743
+ // network-free between health checks, and repeat `health` calls in one session are free too.
1744
+ tool('health', {}, async () => {
1745
+ try {
1746
+ const mcpUpdate = await getCachedMcpUpdateMetadata();
1747
+ return ok({
1748
+ server: getServerUrl(),
1749
+ ok: mcpUpdate.status !== 'unknown',
1750
+ mcpVersion: PACKAGE_VERSION,
1751
+ mcpUpdate,
1692
1752
  });
1693
1753
  } catch (error) { return err(error); }
1694
1754
  });
@@ -1705,7 +1765,6 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1705
1765
  folder: z.string().nullable().optional().describe('[update] folder name (null to remove from folder)'),
1706
1766
  layers: z.array(z.object({}).passthrough()).optional().describe('[update] full layers array replacement. Use ls / to read current layers first.'),
1707
1767
  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.'),
1708
- skipBrowser: z.boolean().optional().describe('[open] skip opening/navigating a browser tab (use when the user already has the project open, e.g. from an invite snippet)'),
1709
1768
  format: z.string().optional().describe('[export] "files" returns {files:[{path,content}]} paginated via limit/offset (compact=true for paths only) instead of writing a local dir (stdio) or returning a download URL (remote).'),
1710
1769
  limit: z.number().optional().describe('[export] max files per page for format="files" (default 100, max 500)'),
1711
1770
  offset: z.number().optional().describe('[export] pagination offset for format="files"'),
@@ -1750,7 +1809,7 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1750
1809
  return ok(data, { structuredContent });
1751
1810
  }
1752
1811
  case 'open': {
1753
- const { projectId, skipBrowser } = args;
1812
+ const { projectId } = args;
1754
1813
  if (!projectId) throw new Error('projectId required for action=open');
1755
1814
  const result = await api('POST', '/api/project/switch', { projectId });
1756
1815
  joinAgentWsRoom(projectId);
@@ -1767,18 +1826,17 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
1767
1826
  } catch { /* fall back to projectId */ }
1768
1827
  setMcpActiveProject(projectId, projectMeta);
1769
1828
  const url = `${base}/project/${projectSlug}`;
1829
+ // Surfacing to the user is the focus mechanism's job now (agent-active ping ->
1830
+ // desktop window / notification+glow for browser tabs) — this used to also force
1831
+ // `exec('open <url>')` on the MCP host machine, an unsolicited GUI action that both
1832
+ // duplicated the focus mechanism and did nothing useful for remote/hosted MCP mode
1833
+ // (no GUI to open on Drafted's own server). `url` is still returned below for the
1834
+ // agent/human to open manually.
1770
1835
  let navigated = 0;
1771
- if (!skipBrowser) {
1772
- try {
1773
- const nav = await api('POST', '/api/project/navigate', { projectId });
1774
- navigated = nav.navigated || 0;
1775
- } catch { /* server may not support navigate yet */ }
1776
- if (navigated === 0) {
1777
- const { exec } = await import('child_process');
1778
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1779
- exec(`${cmd} ${JSON.stringify(url)}`);
1780
- }
1781
- }
1836
+ try {
1837
+ const nav = await api('POST', '/api/project/navigate', { projectId });
1838
+ navigated = nav.navigated || 0;
1839
+ } catch { /* server may not support navigate yet */ }
1782
1840
  // G4/G5 auto-inject (locked design): the project's attached skills + anchors
1783
1841
  // are pushed into the open response within the per-project context budget,
1784
1842
  // replacing the reject-style gate. Prefer the server-computed `priming`
@@ -2318,12 +2376,23 @@ async function getMcpUpdateMetadata() {
2318
2376
  mode,
2319
2377
  distribution: mode === 'stdio' ? 'npm-stdio' : 'hosted-http',
2320
2378
  update: { command: null, helper: null, packageManager: 'npm' },
2321
- restart: { required: false, guidance: 'Drafted MCP update status is unavailable; get_org still succeeded.' },
2379
+ restart: { required: false, guidance: 'Drafted MCP update status is unavailable; this call still succeeded.' },
2322
2380
  checkedAt: null,
2323
2381
  };
2324
2382
  }
2325
2383
  }
2326
2384
 
2385
+ // Process-lifetime cache: `health` is meant to be called every session, so avoid a network
2386
+ // round-trip on repeat calls. `get_org` shares the cache too (same underlying data).
2387
+ let mcpUpdateCache = null; // { data, fetchedAt }
2388
+ const MCP_UPDATE_CACHE_MS = 5 * 60_000;
2389
+ async function getCachedMcpUpdateMetadata() {
2390
+ if (mcpUpdateCache && (Date.now() - mcpUpdateCache.fetchedAt) < MCP_UPDATE_CACHE_MS) return mcpUpdateCache.data;
2391
+ const data = await getMcpUpdateMetadata();
2392
+ mcpUpdateCache = { data, fetchedAt: Date.now() };
2393
+ return data;
2394
+ }
2395
+
2327
2396
 
2328
2397
  tool('get_org', {
2329
2398
  action: z.enum(['get', 'update_mcp']).optional().describe('Default: "get" returns your orgs, the default org, and Google Drive availability. Use "update_mcp" to get explicit installed stdio MCP update instructions. There is no org switching: org derives from the resource you address (projectId/pageId/skillId), and creates/searches take an explicit org param.'),
@@ -2347,7 +2416,7 @@ tool('get_org', {
2347
2416
  const activeOrg = sessionOrgId ? (orgs.find(o => o.id === sessionOrgId) || null) : null;
2348
2417
 
2349
2418
  const googleDrive = await getGoogleDriveAvailability();
2350
- const mcpUpdate = await getMcpUpdateMetadata();
2419
+ const mcpUpdate = await getCachedMcpUpdateMetadata();
2351
2420
 
2352
2421
  let members = [];
2353
2422
  if (sessionOrgId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.14.1",
3
+ "version": "1.14.2",
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": [