slashvibe-mcp 0.6.0 → 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.
Files changed (58) hide show
  1. package/README.md +3 -68
  2. package/cli.js +1 -1
  3. package/host.js +76 -0
  4. package/index.js +223 -83
  5. package/package.json +27 -28
  6. package/resources/presence-board.js +231 -0
  7. package/setup.js +157 -45
  8. package/store/api.js +8 -0
  9. package/tools/help.js +103 -148
  10. package/tools/who.js +31 -3
  11. package/version.json +7 -6
  12. package/analytics.js +0 -107
  13. package/bridges/bridge-monitor.js +0 -388
  14. package/bridges/discord-bot.js +0 -431
  15. package/bridges/farcaster.js +0 -299
  16. package/bridges/telegram.js +0 -261
  17. package/bridges/webhook-health.js +0 -420
  18. package/bridges/webhook-server.js +0 -437
  19. package/bridges/whatsapp.js +0 -441
  20. package/bridges/x-webhook.js +0 -423
  21. package/games/arcade.js +0 -406
  22. package/games/colorguess.js +0 -343
  23. package/games/crossword-words.js +0 -171
  24. package/games/crossword.js +0 -461
  25. package/games/drawing.js +0 -347
  26. package/games/gameroulette.js +0 -300
  27. package/games/gamerouter.js +0 -336
  28. package/games/gamestatus.js +0 -337
  29. package/games/guessnumber.js +0 -209
  30. package/games/hangman.js +0 -279
  31. package/games/memory.js +0 -338
  32. package/games/multiplayer-tictactoe.js +0 -389
  33. package/games/pixelart.js +0 -399
  34. package/games/quickduel.js +0 -354
  35. package/games/riddle.js +0 -371
  36. package/games/rockpaperscissors.js +0 -291
  37. package/games/snake.js +0 -406
  38. package/games/storybuilder.js +0 -343
  39. package/games/tictactoe.js +0 -345
  40. package/games/twentyquestions.js +0 -286
  41. package/games/twotruths.js +0 -207
  42. package/games/werewolf.js +0 -508
  43. package/games/wordassociation.js +0 -247
  44. package/games/wordchain.js +0 -135
  45. package/hooks/check-guest-messages.js +0 -125
  46. package/hooks/check-guest-messages.sh +0 -226
  47. package/intelligence/index.js +0 -45
  48. package/intelligence/interests.js +0 -369
  49. package/lib/tech-detection.js +0 -243
  50. package/protocol/telegram-commands.js +0 -199
  51. package/smart-inbox.js +0 -276
  52. package/store/reservations.js +0 -321
  53. package/store/skills.js +0 -378
  54. package/tools/analytics.js +0 -191
  55. package/tools/matrix-read.js +0 -124
  56. package/tools/matrix-rooms.js +0 -56
  57. package/tools/matrix-send.js +0 -71
  58. package/twitter.js +0 -209
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # vibe-mcp
2
2
 
3
- Social layer for AI-assisted coding. DMs, presence, collaboration, and Matrix multiplayer rooms between developers.
3
+ Social layer for AI-assisted coding. DMs, presence, and collaboration between developers.
4
4
 
5
5
  **Works in:** Claude Code, Cursor, any MCP-compatible IDE
6
6
 
@@ -49,78 +49,14 @@ Add to `~/.claude.json`:
49
49
  - **DMs** - Direct messages between developers
50
50
  - **Memory** - Remember context about connections
51
51
  - **Status** - Share what you're working on
52
- - **Matrix** - Real-time multi-agent collaboration rooms
53
- - **Games** - Play tic-tac-toe while coding
54
-
55
- ---
56
-
57
- ## Matrix Multiplayer Rooms
58
-
59
- /vibe implements the [AIRC protocol](https://airc.chat) for multi-agent, multi-model collaboration over Matrix.
60
-
61
- ### Architecture
62
-
63
- The canonical pattern is a **local HTTP bot per session**. Claude (or any model) communicates with Matrix rooms by curling a lightweight local bot server:
64
-
65
- ```
66
- Claude/Codex session
67
- |
68
- curl http://localhost:7544/send
69
- |
70
- Local HTTP Bot (matrix-bot-sdk)
71
- |
72
- Matrix Homeserver (matrix.slashvibe.dev)
73
- |
74
- Other participants (humans, Claudes, Codex instances)
75
- ```
76
-
77
- ### Bot HTTP Endpoints
78
-
79
- | Endpoint | Method | Description |
80
- |----------|--------|-------------|
81
- | `/health` | GET | Bot health check |
82
- | `/messages` | GET | Read recent messages (query: `roomId`, `limit`) |
83
- | `/send` | POST | Send message to room (body: `roomId`, `text`) |
84
- | `/rooms` | GET | List joined rooms |
85
-
86
- ### MCP Tools (Matrix)
87
-
88
- These tools are the primary collaboration interface:
89
-
90
- | Tool | Description |
91
- |------|-------------|
92
- | `vibe_matrix_rooms` | List rooms the bot has joined |
93
- | `vibe_matrix_read` | Read recent messages from a room |
94
- | `vibe_matrix_send` | Send a message to a room as the bot |
95
-
96
- ### PostToolUse Hook
97
-
98
- The `check-guest-messages.sh` hook runs on every tool use and polls the local Matrix bot for new messages, injecting them into Claude's context automatically. You do not need to manually call `vibe_matrix_read` to see incoming messages.
99
-
100
- ### Planned: matrix-image
101
-
102
- Inline image generation in Matrix rooms via Gemini Nano Banana Pro (`gemini-3-pro-image-preview`). Flow:
103
-
104
- 1. Generate JPEG via Gemini API
105
- 2. Upload to Matrix media repo (`mxc://` URI)
106
- 3. Send `m.image` event with thumbnail to room
107
-
108
- This enables visual collaboration (diagrams, mockups, debug screenshots) directly in the Matrix room.
109
-
110
- ### Homeserver
111
-
112
- - **Server:** `matrix.slashvibe.dev` (Continuwuity 0.5.5 on fly.io)
113
- - **Primary room:** `#vibe-dev:slashvibe.dev`
114
- - **Federation:** `.well-known/matrix/server` and `/client` served from Vercel
115
- - **Encryption:** Unencrypted rooms only (E2E breaks bot pattern)
52
+ - **Play** - Shared games and creative sessions over the DM transport (tic-tac-toe, chess, collaborative poems, exquisite corpse) plus the Weave
116
53
 
117
54
  ---
118
55
 
119
56
  ## Pair/Guest/Call Transport
120
57
 
121
58
  Pairing, live sessions, guest turns, and WebRTC signaling use the supported
122
- REST/KV transport. Matrix rooms are an optional collaboration surface, not a
123
- replacement or migration target for these APIs.
59
+ REST/KV transport.
124
60
 
125
61
  | Surface | Contract |
126
62
  |---------|----------|
@@ -152,7 +88,6 @@ The MCP server connects to `www.slashvibe.dev` for:
152
88
  - User presence and discovery
153
89
  - Message routing (DMs)
154
90
  - Identity verification (GitHub OAuth)
155
- - Matrix federation metadata
156
91
 
157
92
  ## Related
158
93
 
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
 
@@ -341,21 +387,15 @@ const coreTools = {
341
387
  vibe_ship: require('./tools/ship'),
342
388
  vibe_feed: require('./tools/feed'),
343
389
 
344
- // Play — the original January magic: shared experiences over the DM transport.
345
- // vibe_play is the open primitive; game/poem/corpse are hardcoded on-ramps.
390
+ // Play — shared experiences over the DM transport. vibe_play is the open
391
+ // primitive (freeform state courier); game/poem/corpse carry real rule
392
+ // enforcement (chess/tictactoe legality, poem sealing, corpse hidden-state)
393
+ // and read their own legacy payloads — behaviors the primitive does NOT
394
+ // reproduce, so they stay as distinct tools.
346
395
  vibe_play: require('./tools/play'),
347
396
  vibe_game: require('./tools/game'),
348
397
  vibe_poem: require('./tools/poem'),
349
398
  vibe_corpse: require('./tools/corpse'),
350
-
351
- // Matrix rooms — multiplayer
352
- vibe_matrix_rooms: require('./tools/matrix-rooms'),
353
- vibe_matrix_read: require('./tools/matrix-read'),
354
- vibe_matrix_send: require('./tools/matrix-send'),
355
-
356
- // Utility
357
- vibe_help: require('./tools/help'),
358
- vibe_bye: require('./tools/bye'),
359
399
  };
360
400
 
361
401
  // Admin tools (only loaded when VIBE_ADMIN=true)
@@ -367,8 +407,8 @@ const adminTools = process.env.VIBE_ADMIN === 'true' ? {
367
407
  vibe_patterns: require('./tools/patterns'),
368
408
  } : {};
369
409
 
370
- // Combine tools
371
- const tools = { ...coreTools, ...adminTools };
410
+ // Combine tools — kernel always; extras only when opted in
411
+ const tools = { ...kernelTools, ...(EXTRAS_ENABLED ? extraTools : {}), ...adminTools };
372
412
 
373
413
  /**
374
414
  * MCP Protocol Handler
@@ -405,36 +445,133 @@ class VibeMCPServer {
405
445
  async handleRequest(request) {
406
446
  const { method, params, id } = request;
407
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
+
408
479
  switch (method) {
409
- 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);
410
486
  return {
411
487
  jsonrpc: '2.0',
412
488
  id,
413
489
  result: {
414
- protocolVersion: '2024-11-05',
415
- capabilities: { tools: {} },
416
- serverInfo: {
417
- name: 'vibe',
418
- version: '1.0.0',
419
- description: 'Communication layer for Claude Code'
420
- }
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 }
497
+ }
498
+ };
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
421
519
  }
422
520
  };
521
+ }
423
522
 
424
523
  case 'tools/list':
425
- // 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.
426
527
  const allToolDefs = Object.values(tools).map(t => t.definition);
427
- const isListAuthed = authStore.isAuthenticated() || config.hasOAuth();
428
528
  return {
429
529
  jsonrpc: '2.0',
430
530
  id,
431
531
  result: {
432
- tools: isListAuthed
532
+ tools: isAuthed()
433
533
  ? allToolDefs
434
- : 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'
435
552
  }
436
553
  };
437
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'
571
+ }
572
+ };
573
+ }
574
+
438
575
  case 'tools/call':
439
576
  const tool = tools[params.name];
440
577
  if (!tool) {
@@ -446,8 +583,7 @@ class VibeMCPServer {
446
583
  }
447
584
 
448
585
  // Auth gate: auto-detect unauthenticated state and trigger auth flow
449
- const isCallAuthed = authStore.isAuthenticated() || config.hasOAuth();
450
- if (!isCallAuthed && !NO_AUTH_REQUIRED.has(params.name)) {
586
+ if (!isAuthed() && !NO_AUTH_REQUIRED.has(params.name)) {
451
587
  // Auto-trigger init flow — user never needs to know about "vibe init"
452
588
  const actionName = params.name.replace('vibe_', '');
453
589
  const initTool = tools['vibe_init'];
@@ -460,7 +596,7 @@ class VibeMCPServer {
460
596
  const initDisplay = initResult.display || JSON.stringify(initResult, null, 2);
461
597
 
462
598
  // After auth, emit tools/list_changed so Claude sees full toolset
463
- if (authStore.isAuthenticated() || config.hasOAuth()) {
599
+ if (isAuthed()) {
464
600
  global.vibeNotifier?.emitImmediate();
465
601
  }
466
602
 
@@ -507,7 +643,7 @@ class VibeMCPServer {
507
643
  // This triggers Claude to refresh without reconnection
508
644
  const stateChangingTools = [
509
645
  'vibe_dm', 'vibe_reply', 'vibe_status',
510
- 'vibe_ship', 'vibe_matrix_send'
646
+ 'vibe_ship', 'vibe_play', 'vibe_game'
511
647
  ];
512
648
  if (stateChangingTools.includes(params.name)) {
513
649
  // Debounced notification (prevents spam)
@@ -516,27 +652,31 @@ class VibeMCPServer {
516
652
 
517
653
  // After init/start completes auth, emit tools/list_changed
518
654
  // so Claude sees the full toolset (progressive disclosure unlock)
519
- if ((params.name === 'vibe_init' || params.name === 'vibe_start') &&
520
- (authStore.isAuthenticated() || config.hasOAuth())) {
655
+ if ((params.name === 'vibe_init' || params.name === 'vibe_start') && isAuthed()) {
521
656
  global.vibeNotifier?.emitImmediate();
522
657
  }
523
658
 
524
- // 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).
525
662
  let footer = '';
526
663
  if (!SKIP_FOOTER_TOOLS.includes(params.name)) {
664
+ if (AMBIENT_CACHE_BUSTERS.has(params.name)) bustAmbientCache();
527
665
  footer = await getPresenceFooter();
528
666
  }
529
667
 
530
- return {
531
- jsonrpc: '2.0',
532
- id,
533
- result: {
534
- content: [{
535
- type: 'text',
536
- text: (result.display || JSON.stringify(result, null, 2)) + footer
537
- }]
538
- }
668
+ const callResult = {
669
+ content: [{
670
+ type: 'text',
671
+ text: (result.display || JSON.stringify(result, null, 2)) + footer
672
+ }]
539
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 };
540
680
  } catch (e) {
541
681
  return {
542
682
  jsonrpc: '2.0',