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 +1 -1
- package/host.js +76 -0
- package/index.js +217 -75
- package/package.json +7 -7
- package/resources/presence-board.js +231 -0
- package/setup.js +157 -45
- package/store/api.js +8 -0
- package/tools/help.js +59 -24
- package/tools/who.js +31 -3
- package/version.json +9 -13
- package/games/memory.js +0 -338
- package/intelligence/index.js +0 -45
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
|
|
|
@@ -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 = { ...
|
|
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 '
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"mcpName": "io.github.vibecodinginc/vibe",
|
|
5
|
-
"description": "
|
|
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
|
}
|