slashvibe-mcp 0.7.1 → 0.8.0

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/cli.js CHANGED
@@ -24,7 +24,7 @@ if (args.includes('setup')) {
24
24
  require('./setup.js');
25
25
  } else {
26
26
  // Already set up — show status
27
- console.log('/vibe is configured. Restart Claude Code to connect.');
27
+ console.log('/vibe is configured. Restart your coding agent (Claude Code / Codex / Cursor) to connect.');
28
28
  console.log('');
29
29
  console.log('Commands:');
30
30
  console.log(' npx slashvibe-mcp setup — re-run setup wizard');
package/host.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Host — which coding agent is this MCP server living inside?
3
+ *
4
+ * /vibe presence should say not just "someone is here via mcp" but WHICH
5
+ * terminal agent their session runs in (Claude Code, Codex, Cursor, …).
6
+ * That's the proof of the heterogeneous claim: distinct agent types on the
7
+ * buddy list, one MCP server serving all of them.
8
+ *
9
+ * Detection, in priority order:
10
+ * 1. MCP `initialize` clientInfo.name — the host names itself; index.js
11
+ * calls setClientInfo() when the handshake arrives.
12
+ * 2. Environment fingerprints — fallback for hosts whose clientInfo is
13
+ * missing or generic.
14
+ * The raw name is preserved alongside the normalized slug so unknown hosts
15
+ * still show up honestly instead of as "unknown".
16
+ */
17
+
18
+ let clientInfo = null; // { name, version } from the MCP initialize handshake
19
+
20
+ function setClientInfo(info) {
21
+ if (info && typeof info.name === 'string') {
22
+ clientInfo = { name: info.name, version: info.version || null };
23
+ }
24
+ }
25
+
26
+ // Map a client-reported name to a KNOWN host slug, or null if unrecognized.
27
+ // Word-boundary matching so e.g. "customized-client" doesn't hit the zed rule.
28
+ const KNOWN_HOSTS = [
29
+ [/\bclaude\b/, 'claude-code'],
30
+ [/\bcodex\b/, 'codex'],
31
+ [/\bcursor\b/, 'cursor'],
32
+ [/\bwindsurf\b/, 'windsurf'],
33
+ [/\bcline\b/, 'cline'],
34
+ [/\bzed\b/, 'zed'],
35
+ [/\bgemini\b/, 'gemini-cli'],
36
+ ];
37
+
38
+ function knownHost(name) {
39
+ const n = (name || '').toLowerCase();
40
+ if (!n) return null;
41
+ for (const [re, slug] of KNOWN_HOSTS) {
42
+ if (re.test(n)) return slug;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Slug an unknown host name so it still shows up honestly in presence.
48
+ function slugify(name) {
49
+ const n = (name || '').toLowerCase();
50
+ return n.replace(/[^a-z0-9.-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32) || null;
51
+ }
52
+
53
+ function envFingerprint() {
54
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return 'claude-code';
55
+ if (process.env.CODEX_SANDBOX || process.env.CODEX_HOME) return 'codex';
56
+ if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_CHANNEL) return 'cursor';
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * The host agent this server is running inside.
62
+ * Precedence: recognized clientInfo name → env fingerprint → slug of whatever
63
+ * the client called itself → 'terminal'. A generic/unknown clientInfo name
64
+ * (e.g. "mcp-client") must NOT defeat a definitive env fingerprint.
65
+ * @returns {{ agent: string, version: string|null }}
66
+ */
67
+ function getHost() {
68
+ const agent =
69
+ knownHost(clientInfo?.name) ||
70
+ envFingerprint() ||
71
+ slugify(clientInfo?.name) ||
72
+ 'terminal';
73
+ return { agent, version: clientInfo?.version || null };
74
+ }
75
+
76
+ module.exports = { setClientInfo, getHost };
package/index.js CHANGED
@@ -17,9 +17,35 @@ const prompts = require('./prompts');
17
17
  const NotificationEmitter = require('./notification-emitter');
18
18
  const authStore = require('./auth-store');
19
19
  const { apiHeaders } = require('./api-auth');
20
+ const presenceBoard = require('./resources/presence-board');
21
+ const pkg = require('./package.json');
22
+
23
+ // ─── MCP protocol identity (spec 2026-07-28, dual-era) ──────────────────
24
+ // Modern clients (2026-07-28+) declare io.modelcontextprotocol/protocolVersion
25
+ // in each request's _meta and get stateless semantics: server/discover,
26
+ // resultType on results, serverInfo echoed in result _meta. Legacy clients
27
+ // (2025-11-25 and earlier) still open with `initialize`; both eras are served
28
+ // from this one process. Known gap: list_changed notifications are pushed
29
+ // unsolicited (legacy style) — modern subscriptions/listen is not implemented.
30
+ const MODERN_PROTOCOL_VERSION = '2026-07-28';
31
+ const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
32
+ const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
33
+ const META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
34
+ const SERVER_INFO = {
35
+ name: 'vibe',
36
+ version: pkg.version,
37
+ description: 'Presence + messaging for terminal coding agents'
38
+ };
39
+ const SERVER_CAPABILITIES = {
40
+ tools: { listChanged: true },
41
+ resources: {},
42
+ // MCP Apps: ui:// resources the host renders in-conversation.
43
+ // See resources/presence-board.js (declared on vibe_who).
44
+ extensions: { 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] } }
45
+ };
20
46
 
21
47
  // Tools that shouldn't show presence footer (would be redundant/noisy)
22
- const SKIP_FOOTER_TOOLS = ['vibe_init', 'vibe_doctor', 'vibe_test', 'vibe_update', 'vibe_settings', 'vibe_notifications'];
48
+ const SKIP_FOOTER_TOOLS = ['vibe_init', 'vibe_doctor', 'vibe_test', 'vibe_update'];
23
49
 
24
50
  // Progressive disclosure: only these tools are visible before authentication
25
51
  // After auth, the full toolset is revealed via tools/list_changed notification
@@ -39,52 +65,25 @@ const NO_AUTH_REQUIRED = new Set([
39
65
  'vibe_start', 'vibe_init', 'vibe_token', 'vibe_who', 'vibe_help'
40
66
  ]);
41
67
 
42
- // Infer user prompt from tool arguments (for pattern logging)
68
+ // Single source of truth for "is this session signed in" — token or OAuth.
69
+ const isAuthed = () => authStore.isAuthenticated() || config.hasOAuth();
70
+
71
+ // Infer user prompt from tool arguments (for pattern logging).
72
+ // Cases exist only for registered tools whose args improve on the default.
43
73
  function inferPromptFromArgs(toolName, args) {
44
74
  const action = toolName.replace('vibe_', '');
45
75
  const handle = args.handle ? `@${args.handle.replace('@', '')}` : '';
46
76
  const message = args.message ? `"${args.message.slice(0, 50)}..."` : '';
47
- const note = args.note || '';
48
- const mood = args.mood || '';
49
- const reaction = args.reaction || '';
50
77
 
51
78
  switch (action) {
52
79
  case 'start': return 'start vibing';
53
80
  case 'who': return 'who is online';
54
- case 'ping': return `ping ${handle} ${note}`.trim();
55
- case 'react': return `react ${reaction} to ${handle}`.trim();
56
81
  case 'dm': return `message ${handle} ${message}`.trim();
57
82
  case 'inbox': return 'check inbox';
58
- case 'open': return `open thread with ${handle}`;
59
- case 'status': return `set status to ${mood}`;
60
- case 'context': return 'share context';
61
- case 'summarize': return 'summarize session';
62
- case 'bye': return 'end session';
63
- case 'board': return args.content ? 'post to board' : 'view board';
64
- case 'observe': return args.content ? 'record observation' : 'view observations';
65
- case 'invite': return 'generate invite';
66
- case 'echo': return 'send feedback';
67
- case 'x_mentions': return 'check x mentions';
68
- case 'x_reply': return 'reply on x';
69
- case 'handoff': return `handoff task to ${handle}`;
70
- case 'reserve': return args.paths ? `reserve ${args.paths.join(', ')}` : 'reserve files';
71
- case 'release': return `release ${args.reservation_id || 'reservation'}`;
72
- case 'reservations': return 'list reservations';
73
- case 'solo_game': return `play ${args.game || 'game'}`;
74
- case 'game': return `play ${args.game || 'game'} with ${handle}`;
83
+ case 'status': return `set status to ${args.mood || ''}`.trim();
84
+ case 'game': return `play ${args.game || 'game'} with ${handle}`.trim();
75
85
  case 'poem': return args.line ? `add a line to poem with ${handle}` : `emoji poem with ${handle}`;
76
- case 'multiplayer_game': return `multiplayer ${args.game || 'game'}`;
77
- case 'drawing': return args.action ? `drawing ${args.action}` : 'collaborative drawing';
78
- case 'crossword': return `crossword ${args.action || 'daily'}`;
79
- case 'discover': return `discover ${args.command || 'suggest'}`;
80
- case 'suggest_tags': return `suggest tags ${args.command || 'suggest'}`;
81
- case 'settings': return args.mute ? `mute for ${args.mute}` : 'view settings';
82
- case 'create_artifact': return `create ${args.template || 'artifact'}: ${args.title || 'untitled'}`;
83
- case 'view_artifact': return args.slug ? `view artifact ${args.slug}` : `list ${args.list || 'artifacts'}`;
84
- case 'broadcast': return args.action ? `broadcast ${args.action}` : 'broadcast status';
85
- case 'watch': return args.target ? `watch ${args.target}` : 'list live broadcasts';
86
- case 'session': return args.action ? `session ${args.action}` : 'list sessions';
87
- case 'chat': return args.message ? `chat "${args.message.slice(0, 30)}..."` : 'view chat';
86
+ case 'bye': return 'end session';
88
87
  default: return `${action} ${handle}`.trim() || null;
89
88
  }
90
89
  }
@@ -165,17 +164,49 @@ async function fetchNewUnreadDms(handle) {
165
164
  }
166
165
  }
167
166
 
167
+ // Ambient presence data cache. getPresenceFooter runs after EVERY tool call,
168
+ // and presence/unread/live counts don't change sub-10s — so three of its five
169
+ // network calls can ride a short TTL. The guest-message and DM injection
170
+ // fetches are deliberately NOT cached: each delivers content at most once
171
+ // (ack=true / injectedDmIds), so caching would replay or delay injections.
172
+ const AMBIENT_CACHE_TTL_MS = 10000;
173
+ let ambientCache = { at: 0, handle: null, data: null };
174
+ const bustAmbientCache = () => { ambientCache = { at: 0, handle: null, data: null }; };
175
+
176
+ // Tools whose effects change what the ambient footer shows (unread counts,
177
+ // presence, own status, session identity) — they get a fresh fetch instead of
178
+ // the cache. vibe_token is here because it can swap the signed-in account.
179
+ const AMBIENT_CACHE_BUSTERS = new Set([
180
+ 'vibe_start', 'vibe_init', 'vibe_token', 'vibe_dm', 'vibe_reply', 'vibe_inbox',
181
+ 'vibe_status', 'vibe_ship', 'vibe_play', 'vibe_game', 'vibe_bye'
182
+ ]);
183
+
184
+ async function getAmbientPresence(handle) {
185
+ const now = Date.now();
186
+ // Keyed by handle: an in-process identity switch must never serve the
187
+ // previous account's counts, even if the buster list misses a path.
188
+ if (ambientCache.data && ambientCache.handle === handle &&
189
+ now - ambientCache.at < AMBIENT_CACHE_TTL_MS) {
190
+ return ambientCache.data;
191
+ }
192
+ const [users, unreadCount, liveCount] = await Promise.all([
193
+ store.getActiveUsers().catch(() => []),
194
+ store.getUnreadCount(handle).catch(() => 0),
195
+ store.getLiveBroadcastCount().catch(() => 0)
196
+ ]);
197
+ ambientCache = { at: now, handle, data: { users, unreadCount, liveCount } };
198
+ return ambientCache.data;
199
+ }
200
+
168
201
  // Generate ambient presence footer - the room leaks into every response
169
202
  async function getPresenceFooter() {
170
203
  try {
171
204
  const handle = config.getHandle();
172
205
  if (!handle) return '';
173
206
 
174
- // Fetch presence, unread, live broadcasts, guest messages, and new DMs in parallel
175
- const [users, unreadCount, liveCount, guestMessages, newDms] = await Promise.all([
176
- store.getActiveUsers().catch(() => []),
177
- store.getUnreadCount(handle).catch(() => 0),
178
- store.getLiveBroadcastCount().catch(() => 0),
207
+ // Ambient data (cached) + injection channels (always fresh) in parallel
208
+ const [{ users, unreadCount, liveCount }, guestMessages, newDms] = await Promise.all([
209
+ getAmbientPresence(handle),
179
210
  fetchGuestMessages(handle).catch(() => []),
180
211
  fetchNewUnreadDms(handle).catch(() => [])
181
212
  ]);
@@ -307,7 +338,11 @@ async function getPresenceFooter() {
307
338
  // Minimal set: auth + presence + messaging + status + ship.
308
339
  // Less is more. 95 tools → 15. Claude gets confused with too many options.
309
340
  // Removed tools still exist in ./tools/ and can be re-added if needed.
310
- const coreTools = {
341
+ // ─── Kernel ──────────────────────────────────────────────────────────────
342
+ // The minimum viable social surface (0.8 "core mode"): identity + presence +
343
+ // messaging, plus auth plumbing, help, and the sign-off. An agent that sees a
344
+ // handful of tools uses them correctly; one that sees twenty wanders.
345
+ const kernelTools = {
311
346
  // Auth & onboarding
312
347
  vibe_start: require('./tools/start'),
313
348
  vibe_init: require('./tools/init'),
@@ -322,6 +357,17 @@ const coreTools = {
322
357
  vibe_inbox: require('./tools/inbox'),
323
358
  vibe_reply: require('./tools/reply'),
324
359
 
360
+ // Utility
361
+ vibe_help: require('./tools/help'),
362
+ vibe_bye: require('./tools/bye'),
363
+ };
364
+
365
+ // ─── Extras ──────────────────────────────────────────────────────────────
366
+ // The culture layer. Real /vibe, but not kernel — opt in with VIBE_EXTRAS=1
367
+ // in the MCP server env. Handlers (and their files) ship regardless; only the
368
+ // default registration shrinks.
369
+ const EXTRAS_ENABLED = ['1', 'true'].includes(String(process.env.VIBE_EXTRAS || '').toLowerCase());
370
+ const extraTools = {
325
371
  // Received collaboration — land the newcomer mid-conversation with a topical match
326
372
  vibe_intro: require('./tools/intro'),
327
373
 
@@ -350,10 +396,6 @@ const coreTools = {
350
396
  vibe_game: require('./tools/game'),
351
397
  vibe_poem: require('./tools/poem'),
352
398
  vibe_corpse: require('./tools/corpse'),
353
-
354
- // Utility
355
- vibe_help: require('./tools/help'),
356
- vibe_bye: require('./tools/bye'),
357
399
  };
358
400
 
359
401
  // Admin tools (only loaded when VIBE_ADMIN=true)
@@ -365,8 +407,8 @@ const adminTools = process.env.VIBE_ADMIN === 'true' ? {
365
407
  vibe_patterns: require('./tools/patterns'),
366
408
  } : {};
367
409
 
368
- // Combine tools
369
- const tools = { ...coreTools, ...adminTools };
410
+ // Combine tools — kernel always; extras only when opted in
411
+ const tools = { ...kernelTools, ...(EXTRAS_ENABLED ? extraTools : {}), ...adminTools };
370
412
 
371
413
  /**
372
414
  * MCP Protocol Handler
@@ -403,35 +445,132 @@ class VibeMCPServer {
403
445
  async handleRequest(request) {
404
446
  const { method, params, id } = request;
405
447
 
448
+ // Modern era: any request carrying a _meta protocol version is served
449
+ // statelessly. The gate applies to server/discover too — an unsupported
450
+ // version gets -32022 whose `supported` list drives the retry; answering
451
+ // with a success the client didn't ask for breaks future negotiation.
452
+ // (Probes WITHOUT _meta still get a DiscoverResult, so the legacy-era
453
+ // backward-compat probe keeps working.)
454
+ const requestedVersion = params?._meta?.[META_PROTOCOL_VERSION];
455
+ const isModern = requestedVersion !== undefined;
456
+ if (isModern && requestedVersion !== MODERN_PROTOCOL_VERSION) {
457
+ return {
458
+ jsonrpc: '2.0',
459
+ id,
460
+ error: {
461
+ code: -32022,
462
+ message: 'Unsupported protocol version',
463
+ data: { supported: [MODERN_PROTOCOL_VERSION], requested: requestedVersion }
464
+ }
465
+ };
466
+ }
467
+
468
+ const response = await this.dispatch(request);
469
+ if (isModern && response?.result) {
470
+ response.result.resultType = response.result.resultType || 'complete';
471
+ response.result._meta = { [META_SERVER_INFO]: SERVER_INFO, ...(response.result._meta || {}) };
472
+ }
473
+ return response;
474
+ }
475
+
476
+ async dispatch(request) {
477
+ const { method, params, id } = request;
478
+
406
479
  switch (method) {
407
- case 'initialize':
480
+ case 'server/discover':
481
+ // MUST-implement under 2026-07-28; also answers _meta-less probes
482
+ // (the version gate in handleRequest rejects unsupported versions
483
+ // before dispatch reaches here). Modern clients may name themselves
484
+ // here; env fingerprints (host.js) cover the ones that don't.
485
+ require('./host').setClientInfo(params?.clientInfo);
408
486
  return {
409
487
  jsonrpc: '2.0',
410
488
  id,
411
489
  result: {
412
- protocolVersion: '2024-11-05',
413
- capabilities: { tools: {} },
414
- serverInfo: {
415
- name: 'vibe',
416
- version: '1.0.0',
417
- description: 'Communication layer for Claude Code'
418
- }
490
+ resultType: 'complete',
491
+ supportedVersions: [MODERN_PROTOCOL_VERSION, ...LEGACY_PROTOCOL_VERSIONS],
492
+ capabilities: SERVER_CAPABILITIES,
493
+ instructions: 'Social layer for terminal coding agents — identity, presence, and DMs between builders, from Claude Code, Codex, or Cursor. Start with vibe_start (sign in) or vibe_who (see who is online).',
494
+ ttlMs: 3600000,
495
+ cacheScope: 'public',
496
+ _meta: { [META_SERVER_INFO]: SERVER_INFO }
419
497
  }
420
498
  };
421
499
 
500
+ case 'initialize': {
501
+ // Legacy handshake (2025-11-25 and earlier). Echo the client's version
502
+ // when we know it — the previous hardcoded 2024-11-05 forced every
503
+ // client down to the oldest revision; unknown versions get our latest
504
+ // legacy revision per the legacy negotiation rule.
505
+ // The host names itself here (claude-code, codex, cursor, …) —
506
+ // captured so the presence heartbeat can say WHICH agent this
507
+ // session lives in. See host.js.
508
+ require('./host').setClientInfo(params?.clientInfo);
509
+ const requested = params?.protocolVersion;
510
+ return {
511
+ jsonrpc: '2.0',
512
+ id,
513
+ result: {
514
+ protocolVersion: LEGACY_PROTOCOL_VERSIONS.includes(requested)
515
+ ? requested
516
+ : LEGACY_PROTOCOL_VERSIONS[0],
517
+ capabilities: SERVER_CAPABILITIES,
518
+ serverInfo: SERVER_INFO
519
+ }
520
+ };
521
+ }
522
+
422
523
  case 'tools/list':
423
- // Progressive disclosure: show only core tools until authenticated
524
+ // Progressive disclosure: show only core tools until authenticated.
525
+ // Registration-object order is stable across calls and processes, so
526
+ // the deterministic-order SHOULD (2026-07-28) is met without sorting.
424
527
  const allToolDefs = Object.values(tools).map(t => t.definition);
425
- const isListAuthed = authStore.isAuthenticated() || config.hasOAuth();
426
528
  return {
427
529
  jsonrpc: '2.0',
428
530
  id,
429
531
  result: {
430
- tools: isListAuthed
532
+ tools: isAuthed()
431
533
  ? allToolDefs
432
- : allToolDefs.filter(t => PRE_AUTH_TOOLS.has(t.name))
534
+ : allToolDefs.filter(t => PRE_AUTH_TOOLS.has(t.name)),
535
+ // CacheableResult: private (varies with auth state). Pre-auth gets
536
+ // ZERO ttl — sign-in swaps the toolset and modern clients have no
537
+ // subscriptions/listen stream to invalidate through; only the
538
+ // legacy unsolicited list_changed exists. Post-auth may cache.
539
+ ttlMs: isAuthed() ? 60000 : 0,
540
+ cacheScope: 'private'
541
+ }
542
+ };
543
+
544
+ case 'resources/list':
545
+ return {
546
+ jsonrpc: '2.0',
547
+ id,
548
+ result: {
549
+ resources: [presenceBoard.definition],
550
+ ttlMs: 3600000,
551
+ cacheScope: 'public'
552
+ }
553
+ };
554
+
555
+ case 'resources/read': {
556
+ if (params?.uri !== presenceBoard.RESOURCE_URI) {
557
+ // -32602 per 2026-07-28 (was -32002; realigned with JSON-RPC).
558
+ return {
559
+ jsonrpc: '2.0',
560
+ id,
561
+ error: { code: -32602, message: `Resource not found: ${params?.uri}` }
562
+ };
563
+ }
564
+ return {
565
+ jsonrpc: '2.0',
566
+ id,
567
+ result: {
568
+ contents: [presenceBoard.content],
569
+ ttlMs: 3600000,
570
+ cacheScope: 'public'
433
571
  }
434
572
  };
573
+ }
435
574
 
436
575
  case 'tools/call':
437
576
  const tool = tools[params.name];
@@ -444,8 +583,7 @@ class VibeMCPServer {
444
583
  }
445
584
 
446
585
  // Auth gate: auto-detect unauthenticated state and trigger auth flow
447
- const isCallAuthed = authStore.isAuthenticated() || config.hasOAuth();
448
- if (!isCallAuthed && !NO_AUTH_REQUIRED.has(params.name)) {
586
+ if (!isAuthed() && !NO_AUTH_REQUIRED.has(params.name)) {
449
587
  // Auto-trigger init flow — user never needs to know about "vibe init"
450
588
  const actionName = params.name.replace('vibe_', '');
451
589
  const initTool = tools['vibe_init'];
@@ -458,7 +596,7 @@ class VibeMCPServer {
458
596
  const initDisplay = initResult.display || JSON.stringify(initResult, null, 2);
459
597
 
460
598
  // After auth, emit tools/list_changed so Claude sees full toolset
461
- if (authStore.isAuthenticated() || config.hasOAuth()) {
599
+ if (isAuthed()) {
462
600
  global.vibeNotifier?.emitImmediate();
463
601
  }
464
602
 
@@ -514,27 +652,31 @@ class VibeMCPServer {
514
652
 
515
653
  // After init/start completes auth, emit tools/list_changed
516
654
  // so Claude sees the full toolset (progressive disclosure unlock)
517
- if ((params.name === 'vibe_init' || params.name === 'vibe_start') &&
518
- (authStore.isAuthenticated() || config.hasOAuth())) {
655
+ if ((params.name === 'vibe_init' || params.name === 'vibe_start') && isAuthed()) {
519
656
  global.vibeNotifier?.emitImmediate();
520
657
  }
521
658
 
522
- // Add ambient presence footer (unless tool is in skip list)
659
+ // Add ambient presence footer (unless tool is in skip list).
660
+ // State-changing tools bust the cache first so the footer reflects
661
+ // what they just did (e.g. inbox read clears the unread badge).
523
662
  let footer = '';
524
663
  if (!SKIP_FOOTER_TOOLS.includes(params.name)) {
664
+ if (AMBIENT_CACHE_BUSTERS.has(params.name)) bustAmbientCache();
525
665
  footer = await getPresenceFooter();
526
666
  }
527
667
 
528
- return {
529
- jsonrpc: '2.0',
530
- id,
531
- result: {
532
- content: [{
533
- type: 'text',
534
- text: (result.display || JSON.stringify(result, null, 2)) + footer
535
- }]
536
- }
668
+ const callResult = {
669
+ content: [{
670
+ type: 'text',
671
+ text: (result.display || JSON.stringify(result, null, 2)) + footer
672
+ }]
537
673
  };
674
+ // Structured mirror for MCP Apps (the presence board reads this) and
675
+ // modern clients; text-only hosts ignore it.
676
+ if (result.structured) {
677
+ callResult.structuredContent = result.structured;
678
+ }
679
+ return { jsonrpc: '2.0', id, result: callResult };
538
680
  } catch (e) {
539
681
  return {
540
682
  jsonrpc: '2.0',
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "slashvibe-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "mcpName": "io.github.vibecodinginc/vibe",
5
- "description": "Social layer for Claude Code - identity, presence, messaging, and consented live collaboration",
5
+ "description": "Presence + messaging for terminal coding agents (Claude Code, Codex, Cursor) — the /vibe kernel",
6
6
  "main": "index.js",
7
7
  "bin": {
8
8
  "slashvibe-mcp": "./cli.js",
@@ -13,7 +13,8 @@
13
13
  "test": "node --test tools/*.test.js",
14
14
  "test:context": "node --test tools/_work-context.test.js",
15
15
  "pack:check": "node scripts/check-pack-closure.js",
16
- "prepack": "node scripts/check-pack-closure.js",
16
+ "prepack": "node scripts/sync-versions.js && node scripts/check-pack-closure.js",
17
+ "version": "node scripts/sync-versions.js && git add server.json",
17
18
  "lint": "eslint . --ext .js",
18
19
  "lint:fix": "eslint . --ext .js --fix",
19
20
  "test:pack": "node --test test/pack-artifact.test.mjs"
@@ -53,6 +54,7 @@
53
54
  "crypto.js",
54
55
  "discord.js",
55
56
  "games/chess.js",
57
+ "host.js",
56
58
  "index.js",
57
59
  "intelligence/infer.js",
58
60
  "intelligence/patterns.js",
@@ -64,6 +66,7 @@
64
66
  "presence.js",
65
67
  "prompts.js",
66
68
  "protocol/index.js",
69
+ "resources/presence-board.js",
67
70
  "setup.js",
68
71
  "store/api.js",
69
72
  "store/index.js",
@@ -103,8 +106,5 @@
103
106
  "tools/who.js",
104
107
  "version.json"
105
108
  ],
106
- "dependencies": {
107
- "crossword-layout-generator": "^0.1.1",
108
- "node-fetch": "^2.7.0"
109
- }
109
+ "dependencies": {}
110
110
  }