@vibescope/mcp-server 0.4.6 → 0.4.7

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.
@@ -27,7 +27,9 @@ export declare class VibescopeApiClient {
27
27
  private retryConfig;
28
28
  constructor(config: ApiClientConfig);
29
29
  private request;
30
- validateAuth(): Promise<ApiResponse<{
30
+ validateAuth(options?: {
31
+ timeoutMs?: number;
32
+ }): Promise<ApiResponse<{
31
33
  valid: boolean;
32
34
  user_id: string;
33
35
  api_key_id: string;
@@ -47,21 +47,30 @@ export class VibescopeApiClient {
47
47
  retryStatusCodes: config.retry?.retryStatusCodes ?? DEFAULT_RETRY_STATUS_CODES,
48
48
  };
49
49
  }
50
- async request(method, path, body) {
50
+ async request(method, path, body, options) {
51
51
  const url = `${this.baseUrl}${path}`;
52
52
  const { maxRetries, baseDelayMs, maxDelayMs, retryStatusCodes } = this.retryConfig;
53
53
  let lastError = null;
54
54
  let lastResponse = null;
55
55
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
56
+ let timeoutId;
56
57
  try {
57
- const response = await fetch(url, {
58
+ const fetchOptions = {
58
59
  method,
59
60
  headers: {
60
61
  'Content-Type': 'application/json',
61
62
  'X-API-Key': this.apiKey
62
63
  },
63
- body: body ? JSON.stringify(body) : undefined
64
- });
64
+ body: body ? JSON.stringify(body) : undefined,
65
+ };
66
+ if (options?.timeoutMs) {
67
+ const controller = new AbortController();
68
+ timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);
69
+ fetchOptions.signal = controller.signal;
70
+ }
71
+ const response = await fetch(url, fetchOptions);
72
+ if (timeoutId)
73
+ clearTimeout(timeoutId);
65
74
  // Check if we should retry this status code
66
75
  if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
67
76
  lastResponse = response;
@@ -94,7 +103,15 @@ export class VibescopeApiClient {
94
103
  };
95
104
  }
96
105
  catch (err) {
97
- lastError = err instanceof Error ? err : new Error('Network error');
106
+ if (timeoutId)
107
+ clearTimeout(timeoutId);
108
+ // Detect AbortError from timeout
109
+ if (err instanceof Error && err.name === 'AbortError' && options?.timeoutMs) {
110
+ lastError = new Error(`Request timed out after ${options.timeoutMs}ms`);
111
+ }
112
+ else {
113
+ lastError = err instanceof Error ? err : new Error('Network error');
114
+ }
98
115
  // Retry on network errors (connection failures, timeouts)
99
116
  if (attempt < maxRetries) {
100
117
  const delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
@@ -130,10 +147,10 @@ export class VibescopeApiClient {
130
147
  };
131
148
  }
132
149
  // Auth endpoints
133
- async validateAuth() {
150
+ async validateAuth(options) {
134
151
  return this.request('POST', '/api/mcp/auth/validate', {
135
152
  api_key: this.apiKey
136
- });
153
+ }, options);
137
154
  }
138
155
  // Session endpoints
139
156
  async startSession(params) {
package/dist/cli-init.js CHANGED
@@ -236,16 +236,17 @@ function writeJsonFile(path, data) {
236
236
  }
237
237
  function buildMcpServerConfig(apiKey) {
238
238
  const isWindows = platform() === 'win32';
239
+ // Prefer globally installed binary (instant start) with npx fallback
239
240
  if (isWindows) {
240
241
  return {
241
242
  command: 'cmd',
242
- args: ['/c', 'npx', '-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
243
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
243
244
  env: { VIBESCOPE_API_KEY: apiKey },
244
245
  };
245
246
  }
246
247
  return {
247
- command: 'npx',
248
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
248
+ command: 'bash',
249
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
249
250
  env: { VIBESCOPE_API_KEY: apiKey },
250
251
  };
251
252
  }
@@ -7,7 +7,7 @@ import { checkVersion, getLocalVersion } from '../version.js';
7
7
  const PACKAGE_NAME = '@vibescope/mcp-server';
8
8
  export const versionHandlers = {
9
9
  check_mcp_version: async (_args, _ctx) => {
10
- const info = await checkVersion();
10
+ const info = await checkVersion({ bypassCache: true });
11
11
  if (info.error) {
12
12
  return success({
13
13
  current_version: info.current,
package/dist/index.js CHANGED
@@ -141,9 +141,9 @@ initApiClient({ apiKey: API_KEY });
141
141
  // ============================================================================
142
142
  // Authentication
143
143
  // ============================================================================
144
- async function validateApiKey() {
144
+ async function validateApiKey(timeoutMs) {
145
145
  const apiClient = getApiClient();
146
- const response = await apiClient.validateAuth();
146
+ const response = await apiClient.validateAuth(timeoutMs ? { timeoutMs } : undefined);
147
147
  if (!response.ok || !response.data?.valid) {
148
148
  return null;
149
149
  }
@@ -153,6 +153,61 @@ async function validateApiKey() {
153
153
  scope: 'personal', // API handles authorization, scope not needed locally
154
154
  };
155
155
  }
156
+ // Deferred auth: started eagerly but awaited on first tool call
157
+ let deferredAuthPromise = null;
158
+ let resolvedAuth = null;
159
+ let authError = null;
160
+ function startDeferredAuth() {
161
+ deferredAuthPromise = validateApiKey(10000)
162
+ .then((auth) => {
163
+ resolvedAuth = auth;
164
+ if (!auth)
165
+ authError = 'Invalid API key';
166
+ return auth;
167
+ })
168
+ .catch((err) => {
169
+ authError = err instanceof Error ? err.message : 'Auth validation failed';
170
+ return null;
171
+ });
172
+ }
173
+ async function getAuth() {
174
+ // If already resolved, return immediately
175
+ if (resolvedAuth)
176
+ return resolvedAuth;
177
+ // Await the deferred promise
178
+ if (deferredAuthPromise) {
179
+ const auth = await deferredAuthPromise;
180
+ if (auth)
181
+ return auth;
182
+ }
183
+ // Auth failed — return structured error
184
+ const troubleshooting = [
185
+ 'MCP auth validation failed.',
186
+ authError ? `Reason: ${authError}` : '',
187
+ 'Troubleshooting:',
188
+ '1. Check your API key is valid at https://vibescope.dev/dashboard/settings',
189
+ '2. Verify VIBESCOPE_API_KEY environment variable is set correctly',
190
+ '3. Check network connectivity to vibescope.dev',
191
+ '4. Restart Claude Code and try again',
192
+ ].filter(Boolean).join('\n');
193
+ throw new Error(troubleshooting);
194
+ }
195
+ // Deferred update warning: fire-and-forget, delivered on first tool call
196
+ let updateWarningPromise = null;
197
+ let resolvedUpdateWarning = undefined; // undefined = not yet resolved
198
+ function startDeferredUpdateCheck() {
199
+ updateWarningPromise = getUpdateWarning().catch(() => null);
200
+ updateWarningPromise.then((warning) => {
201
+ resolvedUpdateWarning = warning;
202
+ });
203
+ }
204
+ function consumeUpdateWarning() {
205
+ if (resolvedUpdateWarning === undefined)
206
+ return null; // not ready yet
207
+ const warning = resolvedUpdateWarning;
208
+ resolvedUpdateWarning = null; // deliver only once
209
+ return warning;
210
+ }
156
211
  // Tool definitions imported from tools.ts
157
212
  // ============================================================================
158
213
  // Tool Handlers
@@ -205,17 +260,10 @@ async function handleTool(auth, name, args) {
205
260
  // Server Setup
206
261
  // ============================================================================
207
262
  async function main() {
208
- // Validate API key on startup via API
209
- const auth = await validateApiKey();
210
- if (!auth) {
211
- console.error('Invalid API key');
212
- process.exit(1);
213
- }
214
- // Check for updates (non-blocking, with timeout)
215
- const updateWarning = await getUpdateWarning();
216
- const serverInstructions = updateWarning
217
- ? `${updateWarning}\n\nVibescope MCP server - AI project tracking and coordination tools.`
218
- : 'Vibescope MCP server - AI project tracking and coordination tools.';
263
+ // Start auth validation eagerly in background (10s timeout) — don't block startup
264
+ startDeferredAuth();
265
+ // Start update check in background — delivered on first tool call if available
266
+ startDeferredUpdateCheck();
219
267
  const server = new Server({
220
268
  name: 'vibescope',
221
269
  version: '0.1.0',
@@ -223,7 +271,7 @@ async function main() {
223
271
  capabilities: {
224
272
  tools: {},
225
273
  },
226
- instructions: serverInstructions,
274
+ instructions: 'Vibescope MCP server - AI project tracking and coordination tools.',
227
275
  });
228
276
  // List available tools
229
277
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -242,6 +290,22 @@ async function main() {
242
290
  // Handle tool calls
243
291
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
244
292
  const { name, arguments: args } = request.params;
293
+ // Await deferred auth on first tool call (usually already resolved)
294
+ let auth;
295
+ try {
296
+ auth = await getAuth();
297
+ }
298
+ catch (err) {
299
+ return {
300
+ content: [
301
+ {
302
+ type: 'text',
303
+ text: err instanceof Error ? err.message : 'Auth validation failed',
304
+ },
305
+ ],
306
+ isError: true,
307
+ };
308
+ }
245
309
  // Check rate limit
246
310
  const rateCheck = rateLimiter.check(auth.apiKeyId);
247
311
  if (!rateCheck.allowed) {
@@ -267,6 +331,14 @@ async function main() {
267
331
  text: JSON.stringify(result, null, 2),
268
332
  },
269
333
  ];
334
+ // Deliver update warning on first tool call (if available)
335
+ const updateWarning = consumeUpdateWarning();
336
+ if (updateWarning) {
337
+ content.push({
338
+ type: 'text',
339
+ text: `\n--- ${updateWarning} ---`,
340
+ });
341
+ }
270
342
  // Include reminder nudge if applicable
271
343
  const reminder = getReminder(name);
272
344
  if (reminder) {
package/dist/setup.js CHANGED
@@ -176,13 +176,19 @@ export function writeConfig(configPath, config) {
176
176
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
177
177
  }
178
178
  export function generateMcpConfig(apiKey, ide) {
179
- const vibescopeServer = {
180
- command: 'npx',
181
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
182
- env: {
183
- VIBESCOPE_API_KEY: apiKey,
184
- },
185
- };
179
+ // Prefer globally installed binary (instant start) with npx fallback
180
+ const isWindows = platform() === 'win32';
181
+ const vibescopeServer = isWindows
182
+ ? {
183
+ command: 'cmd',
184
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
185
+ env: { VIBESCOPE_API_KEY: apiKey },
186
+ }
187
+ : {
188
+ command: 'bash',
189
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
190
+ env: { VIBESCOPE_API_KEY: apiKey },
191
+ };
186
192
  // Gemini CLI uses a different config format with additional options
187
193
  if (ide.configFormat === 'settings-json') {
188
194
  return {
package/dist/version.d.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * Version checking utilities
3
3
  *
4
4
  * Compares the locally installed version against the latest published
5
- * version on npm to detect available updates.
5
+ * version on npm to detect available updates. Uses a file-based cache
6
+ * (~/.vibescope/version-cache.json) with 1-hour TTL to avoid hitting
7
+ * the npm registry on every startup.
6
8
  */
7
9
  export interface VersionInfo {
8
10
  current: string;
@@ -17,11 +19,15 @@ export declare function getLocalVersion(): string;
17
19
  /**
18
20
  * Fetch the latest published version from npm registry
19
21
  */
20
- export declare function getLatestVersion(): Promise<string | null>;
22
+ export declare function getLatestVersion(options?: {
23
+ bypassCache?: boolean;
24
+ }): Promise<string | null>;
21
25
  /**
22
26
  * Check if an update is available
23
27
  */
24
- export declare function checkVersion(): Promise<VersionInfo>;
28
+ export declare function checkVersion(options?: {
29
+ bypassCache?: boolean;
30
+ }): Promise<VersionInfo>;
25
31
  /**
26
32
  * Get the update warning message for server instructions, or null if up to date
27
33
  */
package/dist/version.js CHANGED
@@ -2,13 +2,19 @@
2
2
  * Version checking utilities
3
3
  *
4
4
  * Compares the locally installed version against the latest published
5
- * version on npm to detect available updates.
5
+ * version on npm to detect available updates. Uses a file-based cache
6
+ * (~/.vibescope/version-cache.json) with 1-hour TTL to avoid hitting
7
+ * the npm registry on every startup.
6
8
  */
7
- import { readFileSync } from 'fs';
8
- import { resolve, dirname } from 'path';
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { resolve, dirname, join } from 'path';
9
11
  import { fileURLToPath } from 'url';
12
+ import { homedir } from 'os';
10
13
  const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@vibescope/mcp-server';
11
14
  const PACKAGE_NAME = '@vibescope/mcp-server';
15
+ const CACHE_DIR = join(homedir(), '.vibescope');
16
+ const CACHE_PATH = join(CACHE_DIR, 'version-cache.json');
17
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
12
18
  /**
13
19
  * Get the current locally installed version from package.json
14
20
  */
@@ -23,13 +29,51 @@ export function getLocalVersion() {
23
29
  return 'unknown';
24
30
  }
25
31
  }
32
+ /**
33
+ * Read version from file-based cache if still valid
34
+ */
35
+ function readVersionCache() {
36
+ try {
37
+ if (!existsSync(CACHE_PATH))
38
+ return null;
39
+ const raw = JSON.parse(readFileSync(CACHE_PATH, 'utf-8'));
40
+ if (Date.now() - raw.fetchedAt < CACHE_TTL_MS) {
41
+ return raw.latestVersion;
42
+ }
43
+ return null; // expired
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ /**
50
+ * Write version to file-based cache
51
+ */
52
+ function writeVersionCache(version) {
53
+ try {
54
+ if (!existsSync(CACHE_DIR)) {
55
+ mkdirSync(CACHE_DIR, { recursive: true });
56
+ }
57
+ const cache = { latestVersion: version, fetchedAt: Date.now() };
58
+ writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2) + '\n');
59
+ }
60
+ catch {
61
+ // Non-critical — silently ignore write failures
62
+ }
63
+ }
26
64
  /**
27
65
  * Fetch the latest published version from npm registry
28
66
  */
29
- export async function getLatestVersion() {
67
+ export async function getLatestVersion(options) {
68
+ // Check cache first (unless bypassed)
69
+ if (!options?.bypassCache) {
70
+ const cached = readVersionCache();
71
+ if (cached)
72
+ return cached;
73
+ }
30
74
  try {
31
75
  const controller = new AbortController();
32
- const timeout = setTimeout(() => controller.abort(), 5000);
76
+ const timeout = setTimeout(() => controller.abort(), 3000);
33
77
  const response = await fetch(`${NPM_REGISTRY_URL}/latest`, {
34
78
  signal: controller.signal,
35
79
  headers: { 'Accept': 'application/json' },
@@ -38,7 +82,11 @@ export async function getLatestVersion() {
38
82
  if (!response.ok)
39
83
  return null;
40
84
  const data = (await response.json());
41
- return data.version ?? null;
85
+ const version = data.version ?? null;
86
+ // Write to cache on successful fetch
87
+ if (version)
88
+ writeVersionCache(version);
89
+ return version;
42
90
  }
43
91
  catch {
44
92
  return null;
@@ -62,9 +110,9 @@ function isNewer(current, latest) {
62
110
  /**
63
111
  * Check if an update is available
64
112
  */
65
- export async function checkVersion() {
113
+ export async function checkVersion(options) {
66
114
  const current = getLocalVersion();
67
- const latest = await getLatestVersion();
115
+ const latest = await getLatestVersion(options);
68
116
  if (!latest) {
69
117
  return {
70
118
  current,
package/docs/TOOLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Auto-generated from tool definitions. Do not edit manually.
4
4
  >
5
- > Generated: 2026-03-01
5
+ > Generated: 2026-03-04
6
6
  >
7
7
  > Total tools: 167
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescope/mcp-server",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "MCP server for Vibescope - AI project tracking tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/api-client.ts CHANGED
@@ -81,22 +81,32 @@ export class VibescopeApiClient {
81
81
  };
82
82
  }
83
83
 
84
- private async request<T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> {
84
+ private async request<T>(method: string, path: string, body?: unknown, options?: { timeoutMs?: number }): Promise<ApiResponse<T>> {
85
85
  const url = `${this.baseUrl}${path}`;
86
86
  const { maxRetries, baseDelayMs, maxDelayMs, retryStatusCodes } = this.retryConfig;
87
87
  let lastError: Error | null = null;
88
88
  let lastResponse: Response | null = null;
89
89
 
90
90
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
91
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
91
92
  try {
92
- const response = await fetch(url, {
93
+ const fetchOptions: RequestInit = {
93
94
  method,
94
95
  headers: {
95
96
  'Content-Type': 'application/json',
96
97
  'X-API-Key': this.apiKey
97
98
  },
98
- body: body ? JSON.stringify(body) : undefined
99
- });
99
+ body: body ? JSON.stringify(body) : undefined,
100
+ };
101
+
102
+ if (options?.timeoutMs) {
103
+ const controller = new AbortController();
104
+ timeoutId = setTimeout(() => controller.abort(), options.timeoutMs);
105
+ fetchOptions.signal = controller.signal;
106
+ }
107
+
108
+ const response = await fetch(url, fetchOptions);
109
+ if (timeoutId) clearTimeout(timeoutId);
100
110
 
101
111
  // Check if we should retry this status code
102
112
  if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
@@ -132,7 +142,13 @@ export class VibescopeApiClient {
132
142
  data
133
143
  };
134
144
  } catch (err) {
135
- lastError = err instanceof Error ? err : new Error('Network error');
145
+ if (timeoutId) clearTimeout(timeoutId);
146
+ // Detect AbortError from timeout
147
+ if (err instanceof Error && err.name === 'AbortError' && options?.timeoutMs) {
148
+ lastError = new Error(`Request timed out after ${options.timeoutMs}ms`);
149
+ } else {
150
+ lastError = err instanceof Error ? err : new Error('Network error');
151
+ }
136
152
  // Retry on network errors (connection failures, timeouts)
137
153
  if (attempt < maxRetries) {
138
154
  const delay = calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs);
@@ -170,7 +186,7 @@ export class VibescopeApiClient {
170
186
  }
171
187
 
172
188
  // Auth endpoints
173
- async validateAuth(): Promise<ApiResponse<{
189
+ async validateAuth(options?: { timeoutMs?: number }): Promise<ApiResponse<{
174
190
  valid: boolean;
175
191
  user_id: string;
176
192
  api_key_id: string;
@@ -178,7 +194,7 @@ export class VibescopeApiClient {
178
194
  }>> {
179
195
  return this.request('POST', '/api/mcp/auth/validate', {
180
196
  api_key: this.apiKey
181
- });
197
+ }, options);
182
198
  }
183
199
 
184
200
  // Session endpoints
package/src/cli-init.ts CHANGED
@@ -275,16 +275,17 @@ function writeJsonFile(path: string, data: Record<string, unknown>): void {
275
275
 
276
276
  function buildMcpServerConfig(apiKey: string): Record<string, unknown> {
277
277
  const isWindows = platform() === 'win32';
278
+ // Prefer globally installed binary (instant start) with npx fallback
278
279
  if (isWindows) {
279
280
  return {
280
281
  command: 'cmd',
281
- args: ['/c', 'npx', '-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
282
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
282
283
  env: { VIBESCOPE_API_KEY: apiKey },
283
284
  };
284
285
  }
285
286
  return {
286
- command: 'npx',
287
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
287
+ command: 'bash',
288
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
288
289
  env: { VIBESCOPE_API_KEY: apiKey },
289
290
  };
290
291
  }
@@ -11,7 +11,7 @@ const PACKAGE_NAME = '@vibescope/mcp-server';
11
11
 
12
12
  export const versionHandlers: HandlerRegistry = {
13
13
  check_mcp_version: async (_args, _ctx) => {
14
- const info = await checkVersion();
14
+ const info = await checkVersion({ bypassCache: true });
15
15
 
16
16
  if (info.error) {
17
17
  return success({
package/src/index.ts CHANGED
@@ -574,9 +574,9 @@ initApiClient({ apiKey: API_KEY });
574
574
  // Authentication
575
575
  // ============================================================================
576
576
 
577
- async function validateApiKey(): Promise<AuthContext | null> {
577
+ async function validateApiKey(timeoutMs?: number): Promise<AuthContext | null> {
578
578
  const apiClient = getApiClient();
579
- const response = await apiClient.validateAuth();
579
+ const response = await apiClient.validateAuth(timeoutMs ? { timeoutMs } : undefined);
580
580
 
581
581
  if (!response.ok || !response.data?.valid) {
582
582
  return null;
@@ -589,6 +589,66 @@ async function validateApiKey(): Promise<AuthContext | null> {
589
589
  };
590
590
  }
591
591
 
592
+ // Deferred auth: started eagerly but awaited on first tool call
593
+ let deferredAuthPromise: Promise<AuthContext | null> | null = null;
594
+ let resolvedAuth: AuthContext | null = null;
595
+ let authError: string | null = null;
596
+
597
+ function startDeferredAuth(): void {
598
+ deferredAuthPromise = validateApiKey(10000)
599
+ .then((auth) => {
600
+ resolvedAuth = auth;
601
+ if (!auth) authError = 'Invalid API key';
602
+ return auth;
603
+ })
604
+ .catch((err) => {
605
+ authError = err instanceof Error ? err.message : 'Auth validation failed';
606
+ return null;
607
+ });
608
+ }
609
+
610
+ async function getAuth(): Promise<AuthContext> {
611
+ // If already resolved, return immediately
612
+ if (resolvedAuth) return resolvedAuth;
613
+
614
+ // Await the deferred promise
615
+ if (deferredAuthPromise) {
616
+ const auth = await deferredAuthPromise;
617
+ if (auth) return auth;
618
+ }
619
+
620
+ // Auth failed — return structured error
621
+ const troubleshooting = [
622
+ 'MCP auth validation failed.',
623
+ authError ? `Reason: ${authError}` : '',
624
+ 'Troubleshooting:',
625
+ '1. Check your API key is valid at https://vibescope.dev/dashboard/settings',
626
+ '2. Verify VIBESCOPE_API_KEY environment variable is set correctly',
627
+ '3. Check network connectivity to vibescope.dev',
628
+ '4. Restart Claude Code and try again',
629
+ ].filter(Boolean).join('\n');
630
+
631
+ throw new Error(troubleshooting);
632
+ }
633
+
634
+ // Deferred update warning: fire-and-forget, delivered on first tool call
635
+ let updateWarningPromise: Promise<string | null> | null = null;
636
+ let resolvedUpdateWarning: string | null | undefined = undefined; // undefined = not yet resolved
637
+
638
+ function startDeferredUpdateCheck(): void {
639
+ updateWarningPromise = getUpdateWarning().catch(() => null);
640
+ updateWarningPromise.then((warning) => {
641
+ resolvedUpdateWarning = warning;
642
+ });
643
+ }
644
+
645
+ function consumeUpdateWarning(): string | null {
646
+ if (resolvedUpdateWarning === undefined) return null; // not ready yet
647
+ const warning = resolvedUpdateWarning;
648
+ resolvedUpdateWarning = null; // deliver only once
649
+ return warning;
650
+ }
651
+
592
652
  // Tool definitions imported from tools.ts
593
653
 
594
654
 
@@ -648,19 +708,11 @@ async function handleTool(
648
708
  // ============================================================================
649
709
 
650
710
  async function main() {
651
- // Validate API key on startup via API
652
- const auth = await validateApiKey();
653
- if (!auth) {
654
- console.error('Invalid API key');
655
- process.exit(1);
656
- }
657
-
658
- // Check for updates (non-blocking, with timeout)
659
- const updateWarning = await getUpdateWarning();
711
+ // Start auth validation eagerly in background (10s timeout) — don't block startup
712
+ startDeferredAuth();
660
713
 
661
- const serverInstructions = updateWarning
662
- ? `${updateWarning}\n\nVibescope MCP server - AI project tracking and coordination tools.`
663
- : 'Vibescope MCP server - AI project tracking and coordination tools.';
714
+ // Start update check in background — delivered on first tool call if available
715
+ startDeferredUpdateCheck();
664
716
 
665
717
  const server = new Server(
666
718
  {
@@ -671,7 +723,7 @@ async function main() {
671
723
  capabilities: {
672
724
  tools: {},
673
725
  },
674
- instructions: serverInstructions,
726
+ instructions: 'Vibescope MCP server - AI project tracking and coordination tools.',
675
727
  }
676
728
  );
677
729
 
@@ -695,6 +747,22 @@ async function main() {
695
747
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
696
748
  const { name, arguments: args } = request.params;
697
749
 
750
+ // Await deferred auth on first tool call (usually already resolved)
751
+ let auth: AuthContext;
752
+ try {
753
+ auth = await getAuth();
754
+ } catch (err) {
755
+ return {
756
+ content: [
757
+ {
758
+ type: 'text',
759
+ text: err instanceof Error ? err.message : 'Auth validation failed',
760
+ },
761
+ ],
762
+ isError: true,
763
+ };
764
+ }
765
+
698
766
  // Check rate limit
699
767
  const rateCheck = rateLimiter.check(auth.apiKeyId);
700
768
  if (!rateCheck.allowed) {
@@ -728,6 +796,15 @@ async function main() {
728
796
  },
729
797
  ];
730
798
 
799
+ // Deliver update warning on first tool call (if available)
800
+ const updateWarning = consumeUpdateWarning();
801
+ if (updateWarning) {
802
+ content.push({
803
+ type: 'text',
804
+ text: `\n--- ${updateWarning} ---`,
805
+ });
806
+ }
807
+
731
808
  // Include reminder nudge if applicable
732
809
  const reminder = getReminder(name);
733
810
  if (reminder) {
package/src/setup.test.ts CHANGED
@@ -186,8 +186,11 @@ describe('Setup module', () => {
186
186
  const mcpServers = config.mcpServers as Record<string, unknown>;
187
187
  const vibescope = mcpServers.vibescope as Record<string, unknown>;
188
188
 
189
- expect(vibescope.command).toBe('npx');
190
- expect(vibescope.args).toContain('@vibescope/mcp-server@latest');
189
+ // Config uses shell wrapper to prefer global binary with npx fallback
190
+ expect(['cmd', 'bash']).toContain(vibescope.command);
191
+ const argsStr = JSON.stringify(vibescope.args);
192
+ expect(argsStr).toContain('vibescope-mcp');
193
+ expect(argsStr).toContain('npx');
191
194
  expect((vibescope.env as Record<string, string>).VIBESCOPE_API_KEY).toBe('test-api-key');
192
195
  // Standard MCP config should NOT have timeout/trust
193
196
  expect(vibescope.timeout).toBeUndefined();
@@ -207,14 +210,16 @@ describe('Setup module', () => {
207
210
  const mcpServers = config.mcpServers as Record<string, unknown>;
208
211
  const vibescope = mcpServers.vibescope as Record<string, unknown>;
209
212
 
210
- expect(vibescope.command).toBe('npx');
211
- expect(vibescope.args).toContain('@vibescope/mcp-server@latest');
213
+ expect(['cmd', 'bash']).toContain(vibescope.command);
214
+ const argsStr = JSON.stringify(vibescope.args);
215
+ expect(argsStr).toContain('vibescope-mcp');
216
+ expect(argsStr).toContain('npx');
212
217
  expect((vibescope.env as Record<string, string>).VIBESCOPE_API_KEY).toBe('test-api-key');
213
218
  expect(vibescope.timeout).toBe(30000);
214
219
  expect(vibescope.trust).toBe(true);
215
220
  });
216
221
 
217
- it('should use correct npx args format', () => {
222
+ it('should use shell wrapper with global binary fallback', () => {
218
223
  const ide: IdeConfig = {
219
224
  name: 'claude-code',
220
225
  displayName: 'Claude Code (CLI)',
@@ -226,8 +231,13 @@ describe('Setup module', () => {
226
231
  const config = generateMcpConfig('my-key', ide);
227
232
  const mcpServers = config.mcpServers as Record<string, unknown>;
228
233
  const vibescope = mcpServers.vibescope as Record<string, unknown>;
234
+ const args = vibescope.args as string[];
229
235
 
230
- expect(vibescope.args).toEqual(['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp']);
236
+ // Should check for global binary first, then fall back to npx
237
+ const shellCmd = args.join(' ');
238
+ expect(shellCmd).toContain('vibescope-mcp');
239
+ expect(shellCmd).toContain('npx');
240
+ expect(shellCmd).toContain('@vibescope/mcp-server@latest');
231
241
  });
232
242
  });
233
243
  });
package/src/setup.ts CHANGED
@@ -210,13 +210,19 @@ export function writeConfig(configPath: string, config: Record<string, unknown>)
210
210
  }
211
211
 
212
212
  export function generateMcpConfig(apiKey: string, ide: IdeConfig): Record<string, unknown> {
213
- const vibescopeServer = {
214
- command: 'npx',
215
- args: ['-y', '-p', '@vibescope/mcp-server@latest', 'vibescope-mcp'],
216
- env: {
217
- VIBESCOPE_API_KEY: apiKey,
218
- },
219
- };
213
+ // Prefer globally installed binary (instant start) with npx fallback
214
+ const isWindows = platform() === 'win32';
215
+ const vibescopeServer = isWindows
216
+ ? {
217
+ command: 'cmd',
218
+ args: ['/c', 'where vibescope-mcp >nul 2>&1 && vibescope-mcp || npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
219
+ env: { VIBESCOPE_API_KEY: apiKey },
220
+ }
221
+ : {
222
+ command: 'bash',
223
+ args: ['-c', 'command -v vibescope-mcp >/dev/null 2>&1 && exec vibescope-mcp || exec npx -y -p @vibescope/mcp-server@latest vibescope-mcp'],
224
+ env: { VIBESCOPE_API_KEY: apiKey },
225
+ };
220
226
 
221
227
  // Gemini CLI uses a different config format with additional options
222
228
  if (ide.configFormat === 'settings-json') {
package/src/version.ts CHANGED
@@ -2,15 +2,26 @@
2
2
  * Version checking utilities
3
3
  *
4
4
  * Compares the locally installed version against the latest published
5
- * version on npm to detect available updates.
5
+ * version on npm to detect available updates. Uses a file-based cache
6
+ * (~/.vibescope/version-cache.json) with 1-hour TTL to avoid hitting
7
+ * the npm registry on every startup.
6
8
  */
7
9
 
8
- import { readFileSync } from 'fs';
9
- import { resolve, dirname } from 'path';
10
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
11
+ import { resolve, dirname, join } from 'path';
10
12
  import { fileURLToPath } from 'url';
13
+ import { homedir } from 'os';
11
14
 
12
15
  const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@vibescope/mcp-server';
13
16
  const PACKAGE_NAME = '@vibescope/mcp-server';
17
+ const CACHE_DIR = join(homedir(), '.vibescope');
18
+ const CACHE_PATH = join(CACHE_DIR, 'version-cache.json');
19
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
20
+
21
+ interface VersionCache {
22
+ latestVersion: string;
23
+ fetchedAt: number; // epoch ms
24
+ }
14
25
 
15
26
  export interface VersionInfo {
16
27
  current: string;
@@ -33,13 +44,50 @@ export function getLocalVersion(): string {
33
44
  }
34
45
  }
35
46
 
47
+ /**
48
+ * Read version from file-based cache if still valid
49
+ */
50
+ function readVersionCache(): string | null {
51
+ try {
52
+ if (!existsSync(CACHE_PATH)) return null;
53
+ const raw = JSON.parse(readFileSync(CACHE_PATH, 'utf-8')) as VersionCache;
54
+ if (Date.now() - raw.fetchedAt < CACHE_TTL_MS) {
55
+ return raw.latestVersion;
56
+ }
57
+ return null; // expired
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Write version to file-based cache
65
+ */
66
+ function writeVersionCache(version: string): void {
67
+ try {
68
+ if (!existsSync(CACHE_DIR)) {
69
+ mkdirSync(CACHE_DIR, { recursive: true });
70
+ }
71
+ const cache: VersionCache = { latestVersion: version, fetchedAt: Date.now() };
72
+ writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2) + '\n');
73
+ } catch {
74
+ // Non-critical — silently ignore write failures
75
+ }
76
+ }
77
+
36
78
  /**
37
79
  * Fetch the latest published version from npm registry
38
80
  */
39
- export async function getLatestVersion(): Promise<string | null> {
81
+ export async function getLatestVersion(options?: { bypassCache?: boolean }): Promise<string | null> {
82
+ // Check cache first (unless bypassed)
83
+ if (!options?.bypassCache) {
84
+ const cached = readVersionCache();
85
+ if (cached) return cached;
86
+ }
87
+
40
88
  try {
41
89
  const controller = new AbortController();
42
- const timeout = setTimeout(() => controller.abort(), 5000);
90
+ const timeout = setTimeout(() => controller.abort(), 3000);
43
91
 
44
92
  const response = await fetch(`${NPM_REGISTRY_URL}/latest`, {
45
93
  signal: controller.signal,
@@ -51,7 +99,12 @@ export async function getLatestVersion(): Promise<string | null> {
51
99
  if (!response.ok) return null;
52
100
 
53
101
  const data = (await response.json()) as { version?: string };
54
- return data.version ?? null;
102
+ const version = data.version ?? null;
103
+
104
+ // Write to cache on successful fetch
105
+ if (version) writeVersionCache(version);
106
+
107
+ return version;
55
108
  } catch {
56
109
  return null;
57
110
  }
@@ -75,9 +128,9 @@ function isNewer(current: string, latest: string): boolean {
75
128
  /**
76
129
  * Check if an update is available
77
130
  */
78
- export async function checkVersion(): Promise<VersionInfo> {
131
+ export async function checkVersion(options?: { bypassCache?: boolean }): Promise<VersionInfo> {
79
132
  const current = getLocalVersion();
80
- const latest = await getLatestVersion();
133
+ const latest = await getLatestVersion(options);
81
134
 
82
135
  if (!latest) {
83
136
  return {