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.
- package/README.md +3 -68
- package/cli.js +1 -1
- package/host.js +76 -0
- package/index.js +223 -83
- package/package.json +27 -28
- package/resources/presence-board.js +231 -0
- package/setup.js +157 -45
- package/store/api.js +8 -0
- package/tools/help.js +103 -148
- package/tools/who.js +31 -3
- package/version.json +7 -6
- package/analytics.js +0 -107
- package/bridges/bridge-monitor.js +0 -388
- package/bridges/discord-bot.js +0 -431
- package/bridges/farcaster.js +0 -299
- package/bridges/telegram.js +0 -261
- package/bridges/webhook-health.js +0 -420
- package/bridges/webhook-server.js +0 -437
- package/bridges/whatsapp.js +0 -441
- package/bridges/x-webhook.js +0 -423
- package/games/arcade.js +0 -406
- package/games/colorguess.js +0 -343
- package/games/crossword-words.js +0 -171
- package/games/crossword.js +0 -461
- package/games/drawing.js +0 -347
- package/games/gameroulette.js +0 -300
- package/games/gamerouter.js +0 -336
- package/games/gamestatus.js +0 -337
- package/games/guessnumber.js +0 -209
- package/games/hangman.js +0 -279
- package/games/memory.js +0 -338
- package/games/multiplayer-tictactoe.js +0 -389
- package/games/pixelart.js +0 -399
- package/games/quickduel.js +0 -354
- package/games/riddle.js +0 -371
- package/games/rockpaperscissors.js +0 -291
- package/games/snake.js +0 -406
- package/games/storybuilder.js +0 -343
- package/games/tictactoe.js +0 -345
- package/games/twentyquestions.js +0 -286
- package/games/twotruths.js +0 -207
- package/games/werewolf.js +0 -508
- package/games/wordassociation.js +0 -247
- package/games/wordchain.js +0 -135
- package/hooks/check-guest-messages.js +0 -125
- package/hooks/check-guest-messages.sh +0 -226
- package/intelligence/index.js +0 -45
- package/intelligence/interests.js +0 -369
- package/lib/tech-detection.js +0 -243
- package/protocol/telegram-commands.js +0 -199
- package/smart-inbox.js +0 -276
- package/store/reservations.js +0 -321
- package/store/skills.js +0 -378
- package/tools/analytics.js +0 -191
- package/tools/matrix-read.js +0 -124
- package/tools/matrix-rooms.js +0 -56
- package/tools/matrix-send.js +0 -71
- 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,
|
|
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
|
-
- **
|
|
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.
|
|
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'
|
|
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
|
-
//
|
|
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 '
|
|
59
|
-
case '
|
|
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 '
|
|
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
|
-
//
|
|
175
|
-
const [users, unreadCount, liveCount, guestMessages, newDms] = await Promise.all([
|
|
176
|
-
|
|
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
|
-
|
|
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 —
|
|
345
|
-
//
|
|
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 = { ...
|
|
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 '
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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', '
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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',
|