@vibescope/mcp-server 0.4.6 → 0.4.8
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/dist/api-client.d.ts +3 -1
- package/dist/api-client.js +24 -7
- package/dist/cli-init.js +4 -3
- package/dist/handlers/discovery.js +6 -0
- package/dist/handlers/tool-docs.js +22 -0
- package/dist/handlers/version.js +1 -1
- package/dist/index.js +86 -14
- package/dist/setup.js +13 -7
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +4 -1
- package/dist/tools/persona-templates.d.ts +8 -0
- package/dist/tools/persona-templates.js +22 -0
- package/dist/tools/tasks.js +8 -0
- package/dist/version.d.ts +9 -3
- package/dist/version.js +56 -8
- package/docs/TOOLS.md +21 -2
- package/package.json +1 -1
- package/src/api-client.ts +23 -7
- package/src/cli-init.ts +4 -3
- package/src/handlers/discovery.ts +6 -0
- package/src/handlers/tool-docs.ts +24 -0
- package/src/handlers/version.ts +1 -1
- package/src/index.ts +92 -15
- package/src/setup.test.ts +16 -6
- package/src/setup.ts +13 -7
- package/src/tools/index.ts +4 -0
- package/src/tools/persona-templates.ts +25 -0
- package/src/tools/tasks.ts +8 -0
- package/src/version.ts +61 -8
package/dist/api-client.d.ts
CHANGED
|
@@ -27,7 +27,9 @@ export declare class VibescopeApiClient {
|
|
|
27
27
|
private retryConfig;
|
|
28
28
|
constructor(config: ApiClientConfig);
|
|
29
29
|
private request;
|
|
30
|
-
validateAuth(
|
|
30
|
+
validateAuth(options?: {
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
}): Promise<ApiResponse<{
|
|
31
33
|
valid: boolean;
|
|
32
34
|
user_id: string;
|
|
33
35
|
api_key_id: string;
|
package/dist/api-client.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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: '
|
|
248
|
-
args: ['-
|
|
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
|
}
|
|
@@ -340,6 +340,12 @@ export const TOOL_CATEGORIES = {
|
|
|
340
340
|
{ name: 'update_mcp_server', brief: 'Self-update the MCP server' },
|
|
341
341
|
],
|
|
342
342
|
},
|
|
343
|
+
persona_templates: {
|
|
344
|
+
description: 'Persona templates — behavioral archetypes that shape agent behavior',
|
|
345
|
+
tools: [
|
|
346
|
+
{ name: 'get_persona_templates', brief: 'List available persona templates for a project' },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
343
349
|
features: {
|
|
344
350
|
description: 'Feature specs — flesh out ideas into full specs before breaking into tasks',
|
|
345
351
|
tools: [
|
|
@@ -1300,6 +1300,17 @@ Read recent project chat messages to stay informed about project communication.
|
|
|
1300
1300
|
- count: Number of messages returned
|
|
1301
1301
|
|
|
1302
1302
|
**Example:** get_project_messages(project_id: "123e4567-e89b-12d3-a456-426614174000", limit: 10)`,
|
|
1303
|
+
post_progress: `# post_progress
|
|
1304
|
+
Post a structured progress update to the project chat. Use at key milestones: starting a task, creating a PR, hitting a blocker, or completing work.
|
|
1305
|
+
|
|
1306
|
+
**Parameters:**
|
|
1307
|
+
- project_id (required): Project UUID
|
|
1308
|
+
- message (required): Progress update message (supports markdown)
|
|
1309
|
+
- type (optional): Update type — info (general), milestone (key achievement), blocker (blocking issue), question (asking for input). Default: info
|
|
1310
|
+
|
|
1311
|
+
**Returns:** Confirmation with message_id and timestamp.
|
|
1312
|
+
|
|
1313
|
+
**Example:** post_progress(project_id: "123e4567-e89b-12d3-a456-426614174000", message: "PR #42 created for auth feature", type: "milestone")`,
|
|
1303
1314
|
// Version management tools
|
|
1304
1315
|
check_mcp_version: `# check_mcp_version
|
|
1305
1316
|
Check for available MCP server updates and version information.
|
|
@@ -1331,6 +1342,17 @@ Self-update the MCP server to the latest available version.
|
|
|
1331
1342
|
**Example:** update_mcp_server()
|
|
1332
1343
|
|
|
1333
1344
|
**Note:** This operation may temporarily disconnect active sessions during restart.`,
|
|
1345
|
+
// Persona template tools
|
|
1346
|
+
get_persona_templates: `# get_persona_templates
|
|
1347
|
+
List available persona templates for a project. Returns both global defaults and project-specific templates.
|
|
1348
|
+
|
|
1349
|
+
**Parameters:**
|
|
1350
|
+
- project_id (required): Project UUID
|
|
1351
|
+
|
|
1352
|
+
**Returns:**
|
|
1353
|
+
- templates: Array of persona templates with id, name, description, focus_areas, icon, is_default, scope (global/project)
|
|
1354
|
+
|
|
1355
|
+
**Example:** get_persona_templates(project_id: "123e4567-e89b-12d3-a456-426614174000")`,
|
|
1334
1356
|
add_feature: `# add_feature
|
|
1335
1357
|
Create a draft feature spec. Features sit between ideas (quick thoughts) and tasks (actionable work).
|
|
1336
1358
|
|
package/dist/handlers/version.js
CHANGED
|
@@ -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
|
-
//
|
|
209
|
-
|
|
210
|
-
if
|
|
211
|
-
|
|
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:
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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/tools/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { connectorTools } from './connectors.js';
|
|
|
32
32
|
import { cloudAgentTools } from './cloud-agents.js';
|
|
33
33
|
import { versionTools } from './version.js';
|
|
34
34
|
import { featureTools } from './features.js';
|
|
35
|
+
import { personaTemplateTools } from './persona-templates.js';
|
|
35
36
|
/**
|
|
36
37
|
* All MCP tool definitions combined
|
|
37
38
|
*/
|
|
@@ -71,5 +72,6 @@ export declare const toolCategories: {
|
|
|
71
72
|
readonly cloudAgents: string[];
|
|
72
73
|
readonly version: string[];
|
|
73
74
|
readonly features: string[];
|
|
75
|
+
readonly personaTemplates: string[];
|
|
74
76
|
};
|
|
75
|
-
export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, };
|
|
77
|
+
export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, personaTemplateTools, };
|
package/dist/tools/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import { cloudAgentTools } from './cloud-agents.js';
|
|
|
32
32
|
import { versionTools } from './version.js';
|
|
33
33
|
import { chatTools } from './chat.js';
|
|
34
34
|
import { featureTools } from './features.js';
|
|
35
|
+
import { personaTemplateTools } from './persona-templates.js';
|
|
35
36
|
/**
|
|
36
37
|
* All MCP tool definitions combined
|
|
37
38
|
*/
|
|
@@ -63,6 +64,7 @@ export const tools = [
|
|
|
63
64
|
...versionTools,
|
|
64
65
|
...chatTools,
|
|
65
66
|
...featureTools,
|
|
67
|
+
...personaTemplateTools,
|
|
66
68
|
];
|
|
67
69
|
/**
|
|
68
70
|
* Build the complete tool list from all domain modules
|
|
@@ -101,6 +103,7 @@ export const toolCategories = {
|
|
|
101
103
|
cloudAgents: cloudAgentTools.map((t) => t.name),
|
|
102
104
|
version: versionTools.map((t) => t.name),
|
|
103
105
|
features: featureTools.map((t) => t.name),
|
|
106
|
+
personaTemplates: personaTemplateTools.map((t) => t.name),
|
|
104
107
|
};
|
|
105
108
|
// Re-export domain tools for selective imports
|
|
106
|
-
export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, };
|
|
109
|
+
export { sessionTools, costTools, discoveryTools, worktreeTools, projectTools, taskTools, progressTools, blockerTools, decisionTools, ideaTools, findingTools, validationTools, deploymentTools, fallbackTools, requestTools, milestoneTools, bodiesOfWorkTools, organizationTools, fileCheckoutTools, roleTools, sprintTools, gitIssueTools, connectorTools, cloudAgentTools, versionTools, featureTools, personaTemplateTools, };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona Template Tool Definitions
|
|
3
|
+
*
|
|
4
|
+
* Tools for managing persona templates:
|
|
5
|
+
* - get_persona_templates
|
|
6
|
+
*/
|
|
7
|
+
export const personaTemplateTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'get_persona_templates',
|
|
10
|
+
description: 'List available persona templates for a project. Returns both global defaults and project-specific templates. Persona templates define behavioral archetypes (e.g., Code Reviewer, QA Engineer) that shape agent behavior.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
project_id: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Project UUID',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ['project_id'],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
];
|
package/dist/tools/tasks.js
CHANGED
|
@@ -222,6 +222,10 @@ WHEN TO USE: If the user gives you work directly (e.g., "implement feature X", "
|
|
|
222
222
|
enum: ['haiku', 'sonnet', 'opus'],
|
|
223
223
|
description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
|
|
224
224
|
},
|
|
225
|
+
assigned_agent_name: {
|
|
226
|
+
type: 'string',
|
|
227
|
+
description: 'Pre-assign this task to a specific agent by persona name. Only that agent will pick it up via get_next_task.',
|
|
228
|
+
},
|
|
225
229
|
},
|
|
226
230
|
required: ['project_id', 'title'],
|
|
227
231
|
},
|
|
@@ -292,6 +296,10 @@ For projects without git branching (trunk-based or none), use skip_worktree_requ
|
|
|
292
296
|
enum: ['haiku', 'sonnet', 'opus'],
|
|
293
297
|
description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
|
|
294
298
|
},
|
|
299
|
+
assigned_agent_name: {
|
|
300
|
+
type: ['string', 'null'],
|
|
301
|
+
description: 'Pre-assign this task to a specific agent by persona name. Set to null to unassign. Only the assigned agent will pick it up via get_next_task.',
|
|
302
|
+
},
|
|
295
303
|
skip_worktree_requirement: {
|
|
296
304
|
type: 'boolean',
|
|
297
305
|
description: 'Skip git_branch requirement for projects without branching workflows (trunk-based or none). Default: false',
|
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(
|
|
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(
|
|
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(),
|
|
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
|
-
|
|
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,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> Auto-generated from tool definitions. Do not edit manually.
|
|
4
4
|
>
|
|
5
|
-
> Generated: 2026-03-
|
|
5
|
+
> Generated: 2026-03-05
|
|
6
6
|
>
|
|
7
|
-
> Total tools:
|
|
7
|
+
> Total tools: 168
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
10
|
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
- [cloud_agents](#cloud-agents) - Cloud agent management and cleanup (3 tools)
|
|
37
37
|
- [chat](#chat) - Project-wide chat channel for agent and user communication (3 tools)
|
|
38
38
|
- [version](#version) - MCP server version management and updates (2 tools)
|
|
39
|
+
- [persona_templates](#persona-templates) - Persona templates — behavioral archetypes that shape agent behavior (1 tools)
|
|
39
40
|
- [features](#features) - Feature specs — flesh out ideas into full specs before breaking into tasks (6 tools)
|
|
40
41
|
|
|
41
42
|
## session
|
|
@@ -380,6 +381,7 @@ WHEN TO USE: If the user gives you work directly (e.g., "implement feature X", "
|
|
|
380
381
|
| `estimated_minutes` | `number` | No | Estimated time to complete in minutes (min: 1) |
|
|
381
382
|
| `blocking` | `boolean` | No | When true, this task blocks all other work until complete (used for deployment finalization) |
|
|
382
383
|
| `model_capability` | `"haiku" | "sonnet" | "opus"` | No | Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning) |
|
|
384
|
+
| `assigned_agent_name` | `string` | No | Pre-assign this task to a specific agent by persona name. Only that agent will pick it up via get_next_task. |
|
|
383
385
|
|
|
384
386
|
---
|
|
385
387
|
|
|
@@ -417,6 +419,7 @@ For projects without git branching (trunk-based or none), use skip_worktree_requ
|
|
|
417
419
|
| `worktree_path` | `string` | No | Git worktree path for this task (e.g., "../project-task-abc123"). Store this for cleanup tracking across sessions. |
|
|
418
420
|
| `worktree_hostname` | `string` | No | Machine hostname where worktree was created (os.hostname()). Required with worktree_path to enable machine-aware cleanup. |
|
|
419
421
|
| `model_capability` | `"haiku" | "sonnet" | "opus"` | No | Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning) |
|
|
422
|
+
| `assigned_agent_name` | `string,null` | No | Pre-assign this task to a specific agent by persona name. Set to null to unassign. Only the assigned agent will pick it up via get_next_task. |
|
|
420
423
|
| `skip_worktree_requirement` | `boolean` | No | Skip git_branch requirement for projects without branching workflows (trunk-based or none). Default: false |
|
|
421
424
|
| `session_id` | `string` | No | Session ID from start_work_session. Required for cloud agents using mcporter (session context is not preserved between calls). Links the task to your agent on the dashboard. |
|
|
422
425
|
|
|
@@ -2574,6 +2577,22 @@ Update the Vibescope MCP server to the latest version. Runs npm install to fetch
|
|
|
2574
2577
|
|
|
2575
2578
|
---
|
|
2576
2579
|
|
|
2580
|
+
## persona_templates
|
|
2581
|
+
|
|
2582
|
+
*Persona templates — behavioral archetypes that shape agent behavior*
|
|
2583
|
+
|
|
2584
|
+
### get_persona_templates
|
|
2585
|
+
|
|
2586
|
+
List available persona templates for a project. Returns both global defaults and project-specific templates. Persona templates define behavioral archetypes (e.g., Code Reviewer, QA Engineer) that shape agent behavior.
|
|
2587
|
+
|
|
2588
|
+
**Parameters:**
|
|
2589
|
+
|
|
2590
|
+
| Parameter | Type | Required | Description |
|
|
2591
|
+
|-----------|------|----------|-------------|
|
|
2592
|
+
| `project_id` | `string` | Yes | Project UUID |
|
|
2593
|
+
|
|
2594
|
+
---
|
|
2595
|
+
|
|
2577
2596
|
## features
|
|
2578
2597
|
|
|
2579
2598
|
*Feature specs — flesh out ideas into full specs before breaking into tasks*
|
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
|
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: '
|
|
287
|
-
args: ['-
|
|
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
|
}
|
|
@@ -348,6 +348,12 @@ export const TOOL_CATEGORIES: Record<string, { description: string; tools: Array
|
|
|
348
348
|
{ name: 'update_mcp_server', brief: 'Self-update the MCP server' },
|
|
349
349
|
],
|
|
350
350
|
},
|
|
351
|
+
persona_templates: {
|
|
352
|
+
description: 'Persona templates — behavioral archetypes that shape agent behavior',
|
|
353
|
+
tools: [
|
|
354
|
+
{ name: 'get_persona_templates', brief: 'List available persona templates for a project' },
|
|
355
|
+
],
|
|
356
|
+
},
|
|
351
357
|
features: {
|
|
352
358
|
description: 'Feature specs — flesh out ideas into full specs before breaking into tasks',
|
|
353
359
|
tools: [
|
|
@@ -1464,6 +1464,18 @@ Read recent project chat messages to stay informed about project communication.
|
|
|
1464
1464
|
|
|
1465
1465
|
**Example:** get_project_messages(project_id: "123e4567-e89b-12d3-a456-426614174000", limit: 10)`,
|
|
1466
1466
|
|
|
1467
|
+
post_progress: `# post_progress
|
|
1468
|
+
Post a structured progress update to the project chat. Use at key milestones: starting a task, creating a PR, hitting a blocker, or completing work.
|
|
1469
|
+
|
|
1470
|
+
**Parameters:**
|
|
1471
|
+
- project_id (required): Project UUID
|
|
1472
|
+
- message (required): Progress update message (supports markdown)
|
|
1473
|
+
- type (optional): Update type — info (general), milestone (key achievement), blocker (blocking issue), question (asking for input). Default: info
|
|
1474
|
+
|
|
1475
|
+
**Returns:** Confirmation with message_id and timestamp.
|
|
1476
|
+
|
|
1477
|
+
**Example:** post_progress(project_id: "123e4567-e89b-12d3-a456-426614174000", message: "PR #42 created for auth feature", type: "milestone")`,
|
|
1478
|
+
|
|
1467
1479
|
// Version management tools
|
|
1468
1480
|
check_mcp_version: `# check_mcp_version
|
|
1469
1481
|
Check for available MCP server updates and version information.
|
|
@@ -1497,6 +1509,18 @@ Self-update the MCP server to the latest available version.
|
|
|
1497
1509
|
|
|
1498
1510
|
**Note:** This operation may temporarily disconnect active sessions during restart.`,
|
|
1499
1511
|
|
|
1512
|
+
// Persona template tools
|
|
1513
|
+
get_persona_templates: `# get_persona_templates
|
|
1514
|
+
List available persona templates for a project. Returns both global defaults and project-specific templates.
|
|
1515
|
+
|
|
1516
|
+
**Parameters:**
|
|
1517
|
+
- project_id (required): Project UUID
|
|
1518
|
+
|
|
1519
|
+
**Returns:**
|
|
1520
|
+
- templates: Array of persona templates with id, name, description, focus_areas, icon, is_default, scope (global/project)
|
|
1521
|
+
|
|
1522
|
+
**Example:** get_persona_templates(project_id: "123e4567-e89b-12d3-a456-426614174000")`,
|
|
1523
|
+
|
|
1500
1524
|
add_feature: `# add_feature
|
|
1501
1525
|
Create a draft feature spec. Features sit between ideas (quick thoughts) and tasks (actionable work).
|
|
1502
1526
|
|
package/src/handlers/version.ts
CHANGED
|
@@ -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
|
-
//
|
|
652
|
-
|
|
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
|
-
|
|
662
|
-
|
|
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:
|
|
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
|
-
|
|
190
|
-
expect(
|
|
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)
|
|
211
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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/tools/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { cloudAgentTools } from './cloud-agents.js';
|
|
|
36
36
|
import { versionTools } from './version.js';
|
|
37
37
|
import { chatTools } from './chat.js';
|
|
38
38
|
import { featureTools } from './features.js';
|
|
39
|
+
import { personaTemplateTools } from './persona-templates.js';
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* All MCP tool definitions combined
|
|
@@ -68,6 +69,7 @@ export const tools: Tool[] = [
|
|
|
68
69
|
...versionTools,
|
|
69
70
|
...chatTools,
|
|
70
71
|
...featureTools,
|
|
72
|
+
...personaTemplateTools,
|
|
71
73
|
];
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -108,6 +110,7 @@ export const toolCategories = {
|
|
|
108
110
|
cloudAgents: cloudAgentTools.map((t) => t.name),
|
|
109
111
|
version: versionTools.map((t) => t.name),
|
|
110
112
|
features: featureTools.map((t) => t.name),
|
|
113
|
+
personaTemplates: personaTemplateTools.map((t) => t.name),
|
|
111
114
|
} as const;
|
|
112
115
|
|
|
113
116
|
// Re-export domain tools for selective imports
|
|
@@ -138,4 +141,5 @@ export {
|
|
|
138
141
|
cloudAgentTools,
|
|
139
142
|
versionTools,
|
|
140
143
|
featureTools,
|
|
144
|
+
personaTemplateTools,
|
|
141
145
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona Template Tool Definitions
|
|
3
|
+
*
|
|
4
|
+
* Tools for managing persona templates:
|
|
5
|
+
* - get_persona_templates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Tool } from './types.js';
|
|
9
|
+
|
|
10
|
+
export const personaTemplateTools: Tool[] = [
|
|
11
|
+
{
|
|
12
|
+
name: 'get_persona_templates',
|
|
13
|
+
description: 'List available persona templates for a project. Returns both global defaults and project-specific templates. Persona templates define behavioral archetypes (e.g., Code Reviewer, QA Engineer) that shape agent behavior.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
project_id: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Project UUID',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ['project_id'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
package/src/tools/tasks.ts
CHANGED
|
@@ -225,6 +225,10 @@ WHEN TO USE: If the user gives you work directly (e.g., "implement feature X", "
|
|
|
225
225
|
enum: ['haiku', 'sonnet', 'opus'],
|
|
226
226
|
description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
|
|
227
227
|
},
|
|
228
|
+
assigned_agent_name: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Pre-assign this task to a specific agent by persona name. Only that agent will pick it up via get_next_task.',
|
|
231
|
+
},
|
|
228
232
|
},
|
|
229
233
|
required: ['project_id', 'title'],
|
|
230
234
|
},
|
|
@@ -295,6 +299,10 @@ For projects without git branching (trunk-based or none), use skip_worktree_requ
|
|
|
295
299
|
enum: ['haiku', 'sonnet', 'opus'],
|
|
296
300
|
description: 'Recommended model capability: haiku (simple tasks), sonnet (standard), opus (complex reasoning)',
|
|
297
301
|
},
|
|
302
|
+
assigned_agent_name: {
|
|
303
|
+
type: ['string', 'null'],
|
|
304
|
+
description: 'Pre-assign this task to a specific agent by persona name. Set to null to unassign. Only the assigned agent will pick it up via get_next_task.',
|
|
305
|
+
},
|
|
298
306
|
skip_worktree_requirement: {
|
|
299
307
|
type: 'boolean',
|
|
300
308
|
description: 'Skip git_branch requirement for projects without branching workflows (trunk-based or none). Default: false',
|
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(),
|
|
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
|
-
|
|
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 {
|