@bragduck/mcp 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/index.js +1856 -117
  2. package/dist/index.js.map +1 -1
  3. package/package.json +6 -4
  4. package/dist/api-client.d.ts +0 -29
  5. package/dist/api-client.js +0 -146
  6. package/dist/api-client.js.map +0 -1
  7. package/dist/auth.d.ts +0 -8
  8. package/dist/auth.js +0 -202
  9. package/dist/auth.js.map +0 -1
  10. package/dist/credentials.d.ts +0 -31
  11. package/dist/credentials.js +0 -150
  12. package/dist/credentials.js.map +0 -1
  13. package/dist/index.d.ts +0 -2
  14. package/dist/org-preference.d.ts +0 -7
  15. package/dist/org-preference.js +0 -22
  16. package/dist/org-preference.js.map +0 -1
  17. package/dist/tools/link-brag-to-key-result.d.ts +0 -30
  18. package/dist/tools/link-brag-to-key-result.js +0 -81
  19. package/dist/tools/link-brag-to-key-result.js.map +0 -1
  20. package/dist/tools/list-brags.d.ts +0 -53
  21. package/dist/tools/list-brags.js +0 -110
  22. package/dist/tools/list-brags.js.map +0 -1
  23. package/dist/tools/list-objectives.d.ts +0 -15
  24. package/dist/tools/list-objectives.js +0 -66
  25. package/dist/tools/list-objectives.js.map +0 -1
  26. package/dist/tools/list-organisations.d.ts +0 -15
  27. package/dist/tools/list-organisations.js +0 -73
  28. package/dist/tools/list-organisations.js.map +0 -1
  29. package/dist/tools/log-brag.d.ts +0 -88
  30. package/dist/tools/log-brag.js +0 -175
  31. package/dist/tools/log-brag.js.map +0 -1
  32. package/dist/tools/log-story.d.ts +0 -76
  33. package/dist/tools/log-story.js +0 -116
  34. package/dist/tools/log-story.js.map +0 -1
  35. package/dist/tools/login.d.ts +0 -24
  36. package/dist/tools/login.js +0 -46
  37. package/dist/tools/login.js.map +0 -1
  38. package/dist/tools/preview-brag.d.ts +0 -71
  39. package/dist/tools/preview-brag.js +0 -103
  40. package/dist/tools/preview-brag.js.map +0 -1
  41. package/dist/tools/quick-brag.d.ts +0 -38
  42. package/dist/tools/quick-brag.js +0 -143
  43. package/dist/tools/quick-brag.js.map +0 -1
  44. package/dist/tools/save-org-preference.d.ts +0 -30
  45. package/dist/tools/save-org-preference.js +0 -47
  46. package/dist/tools/save-org-preference.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,126 +1,1865 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
- import { loginTool, handleLogin } from './tools/login.js';
6
- import { logBragTool, handleLogBrag } from './tools/log-brag.js';
7
- import { quickBragTool, handleQuickBrag } from './tools/quick-brag.js';
8
- import { handlePreviewBrag } from './tools/preview-brag.js';
9
- import { listBragsTool, handleListBrags } from './tools/list-brags.js';
10
- import { listObjectivesTool, handleListObjectives } from './tools/list-objectives.js';
11
- import { linkBragToKeyResultTool, handleLinkBragToKeyResult } from './tools/link-brag-to-key-result.js';
12
- import { listOrganisationsTool, handleListOrganisations } from './tools/list-organisations.js';
13
- import { saveOrgPreferenceTool, handleSaveOrgPreference } from './tools/save-org-preference.js';
14
- import { logStoryTool, handleLogStory } from './tools/log-story.js';
15
- const server = new Server({
16
- name: 'bragduck',
17
- version: '1.0.0',
18
- }, {
19
- capabilities: {
20
- tools: {},
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ErrorCode,
9
+ ListToolsRequestSchema,
10
+ McpError
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+
13
+ // src/auth.ts
14
+ import { randomBytes as randomBytes2 } from "crypto";
15
+ import { createServer } from "http";
16
+ import { parse } from "url";
17
+ import { exec } from "child_process";
18
+ import { promisify } from "util";
19
+
20
+ // src/credentials.ts
21
+ import { createDecipheriv, scryptSync, randomBytes, createCipheriv } from "crypto";
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
23
+ import { homedir } from "os";
24
+ import { join } from "path";
25
+ import Conf from "conf";
26
+ var APP_NAME = "bragduck";
27
+ var CREDENTIALS_DIR = ".bragduck";
28
+ var CREDENTIALS_FILE = "credentials.enc";
29
+ var ENCRYPTION = {
30
+ ALGORITHM: "aes-256-gcm",
31
+ KEY_LENGTH: 32,
32
+ IV_LENGTH: 16,
33
+ AUTH_TAG_LENGTH: 16,
34
+ SALT_LENGTH: 32
35
+ };
36
+ function deriveEncryptionKey(salt) {
37
+ const password = `${APP_NAME}-${homedir()}-${process.platform}`;
38
+ return scryptSync(password, salt, ENCRYPTION.KEY_LENGTH);
39
+ }
40
+ function decrypt(encryptedData, key) {
41
+ const decipher = createDecipheriv(
42
+ ENCRYPTION.ALGORITHM,
43
+ key,
44
+ Buffer.from(encryptedData.iv, "hex"),
45
+ { authTagLength: ENCRYPTION.AUTH_TAG_LENGTH }
46
+ );
47
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex"));
48
+ let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8");
49
+ decrypted += decipher.final("utf8");
50
+ return decrypted;
51
+ }
52
+ function encrypt(data, key) {
53
+ const iv = randomBytes(ENCRYPTION.IV_LENGTH);
54
+ const cipher = createCipheriv(ENCRYPTION.ALGORITHM, key, iv, {
55
+ authTagLength: ENCRYPTION.AUTH_TAG_LENGTH
56
+ });
57
+ let encrypted = cipher.update(data, "utf8", "hex");
58
+ encrypted += cipher.final("hex");
59
+ const authTag = cipher.getAuthTag();
60
+ return {
61
+ encrypted,
62
+ iv: iv.toString("hex"),
63
+ authTag: authTag.toString("hex"),
64
+ salt: ""
65
+ };
66
+ }
67
+ function readStoredCredentials() {
68
+ const credentialsPath = join(homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
69
+ if (!existsSync(credentialsPath)) {
70
+ return null;
71
+ }
72
+ try {
73
+ const encryptedData = JSON.parse(
74
+ readFileSync(credentialsPath, "utf8")
75
+ );
76
+ const salt = Buffer.from(encryptedData.salt, "hex");
77
+ const key = deriveEncryptionKey(salt);
78
+ const decrypted = decrypt(encryptedData, key);
79
+ return JSON.parse(decrypted);
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ function writeStoredCredentials(credentials) {
85
+ const credentialsDir = join(homedir(), CREDENTIALS_DIR);
86
+ const credentialsPath = join(credentialsDir, CREDENTIALS_FILE);
87
+ if (!existsSync(credentialsDir)) {
88
+ mkdirSync(credentialsDir, { recursive: true, mode: 448 });
89
+ }
90
+ const data = JSON.stringify(credentials);
91
+ const salt = randomBytes(ENCRYPTION.SALT_LENGTH);
92
+ const key = deriveEncryptionKey(salt);
93
+ const encrypted = encrypt(data, key);
94
+ encrypted.salt = salt.toString("hex");
95
+ writeFileSync(credentialsPath, JSON.stringify(encrypted), {
96
+ mode: 384,
97
+ encoding: "utf8"
98
+ });
99
+ }
100
+ function getAccessToken() {
101
+ const credentials = readStoredCredentials();
102
+ if (!credentials) return null;
103
+ if (credentials.accessToken && !credentials.services) {
104
+ return credentials.accessToken;
105
+ }
106
+ return credentials.services?.bragduck?.accessToken || null;
107
+ }
108
+ function getRefreshToken() {
109
+ const credentials = readStoredCredentials();
110
+ if (!credentials) return null;
111
+ if (credentials.accessToken && !credentials.services) {
112
+ return credentials.refreshToken || null;
113
+ }
114
+ return credentials.services?.bragduck?.refreshToken || null;
115
+ }
116
+ function storeCredentials(creds) {
117
+ let existing = readStoredCredentials();
118
+ if (existing && existing.accessToken && !existing.services) {
119
+ existing = {
120
+ services: {
121
+ bragduck: {
122
+ accessToken: existing.accessToken,
123
+ refreshToken: existing.refreshToken,
124
+ expiresAt: existing.expiresAt
125
+ }
126
+ }
127
+ };
128
+ }
129
+ const updated = {
130
+ ...existing,
131
+ services: {
132
+ ...existing?.services,
133
+ bragduck: creds
134
+ }
135
+ };
136
+ writeStoredCredentials(updated);
137
+ }
138
+ function getUserInfo() {
139
+ try {
140
+ const config = new Conf({ projectName: APP_NAME });
141
+ return config.get("userInfo") || null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+ function storeUserInfo(userInfo) {
147
+ const config = new Conf({ projectName: APP_NAME });
148
+ config.set("userInfo", userInfo);
149
+ }
150
+
151
+ // src/auth.ts
152
+ var execAsync = promisify(exec);
153
+ var API_BASE_URL = process.env.BRAGDUCK_API_URL || "https://api.bragduck.com";
154
+ var OAUTH_CLIENT_ID = "bragduck-cli";
155
+ var CALLBACK_PATH = "/callback";
156
+ var TIMEOUT_MS = 12e4;
157
+ var MIN_PORT = 8010;
158
+ var MAX_PORT = 8014;
159
+ async function findAvailablePort() {
160
+ for (let port = MIN_PORT; port <= MAX_PORT; port++) {
161
+ try {
162
+ await new Promise((resolve, reject) => {
163
+ const testServer = createServer();
164
+ testServer.once("error", reject);
165
+ testServer.once("listening", () => {
166
+ testServer.close(() => resolve());
167
+ });
168
+ testServer.listen(port, "127.0.0.1");
169
+ });
170
+ return port;
171
+ } catch {
172
+ continue;
173
+ }
174
+ }
175
+ throw new Error(`No available ports found in range ${MIN_PORT}-${MAX_PORT}`);
176
+ }
177
+ async function openBrowser(url) {
178
+ const platform = process.platform;
179
+ let command;
180
+ switch (platform) {
181
+ case "darwin":
182
+ command = `open "${url}"`;
183
+ break;
184
+ case "win32":
185
+ command = `start "" "${url}"`;
186
+ break;
187
+ default:
188
+ command = `xdg-open "${url}"`;
189
+ break;
190
+ }
191
+ await execAsync(command);
192
+ }
193
+ var SUCCESS_HTML = `<!DOCTYPE html>
194
+ <html><head><meta charset="UTF-8"><title>BragDuck - Authenticated</title>
195
+ <style>
196
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
197
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0;
198
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
199
+ .container { text-align: center; padding: 2rem; background: rgba(255,255,255,0.1);
200
+ border-radius: 1rem; backdrop-filter: blur(10px); max-width: 500px; }
201
+ h1 { font-size: 2rem; margin: 0 0 1rem; }
202
+ .check { font-size: 4rem; animation: pop 0.3s ease-out; }
203
+ p { font-size: 1.1rem; opacity: 0.9; }
204
+ @keyframes pop { from { transform: scale(0); } to { transform: scale(1); } }
205
+ </style></head><body>
206
+ <div class="container">
207
+ <div class="check">&#10003;</div>
208
+ <h1>Authentication Successful</h1>
209
+ <p>You can close this window and return to your editor.</p>
210
+ </div></body></html>`;
211
+ var ERROR_HTML = (error) => `<!DOCTYPE html>
212
+ <html><head><meta charset="UTF-8"><title>BragDuck - Auth Failed</title>
213
+ <style>
214
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
215
+ display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0;
216
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; }
217
+ .container { text-align: center; padding: 2rem; background: rgba(255,255,255,0.1);
218
+ border-radius: 1rem; backdrop-filter: blur(10px); max-width: 500px; }
219
+ h1 { font-size: 2rem; margin: 0 0 1rem; }
220
+ .err { font-size: 4rem; }
221
+ p { font-size: 1.1rem; opacity: 0.9; }
222
+ .details { background: rgba(0,0,0,0.2); padding: 1rem; border-radius: 0.5rem;
223
+ font-family: monospace; font-size: 0.9rem; margin-top: 1rem; }
224
+ </style></head><body>
225
+ <div class="container">
226
+ <div class="err">&#10007;</div>
227
+ <h1>Authentication Failed</h1>
228
+ <p>Please try again.</p>
229
+ <div class="details">${error}</div>
230
+ </div></body></html>`;
231
+ async function exchangeCodeForToken(code, callbackUrl) {
232
+ const response = await fetch(`${API_BASE_URL}/v1/auth/cli/token`, {
233
+ method: "POST",
234
+ headers: { "Content-Type": "application/json" },
235
+ body: JSON.stringify({
236
+ code,
237
+ redirect_uri: callbackUrl,
238
+ client_id: OAUTH_CLIENT_ID,
239
+ grant_type: "authorization_code"
240
+ })
241
+ });
242
+ if (!response.ok) {
243
+ const errorData = await response.json().catch(() => ({}));
244
+ throw new Error(errorData.message || `Token exchange failed: ${response.statusText}`);
245
+ }
246
+ return response.json();
247
+ }
248
+ async function login() {
249
+ const state = randomBytes2(32).toString("hex");
250
+ const port = await findAvailablePort();
251
+ const callbackUrl = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
252
+ const authParams = new URLSearchParams({
253
+ client_id: OAUTH_CLIENT_ID,
254
+ redirect_uri: callbackUrl,
255
+ state
256
+ });
257
+ const authUrl = `${API_BASE_URL}/v1/auth/cli/initiate?${authParams.toString()}`;
258
+ return new Promise((resolve, reject) => {
259
+ let server2 = null;
260
+ let timeoutId = null;
261
+ const cleanup = () => {
262
+ if (timeoutId) globalThis.clearTimeout(timeoutId);
263
+ if (server2) {
264
+ if (typeof server2.closeAllConnections === "function") {
265
+ server2.closeAllConnections();
266
+ }
267
+ server2.close();
268
+ server2.unref();
269
+ }
270
+ };
271
+ const handleRequest = async (req, res) => {
272
+ const parsedUrl = parse(req.url || "", true);
273
+ if (parsedUrl.pathname !== CALLBACK_PATH) {
274
+ res.writeHead(404, { "Content-Type": "text/plain" });
275
+ res.end("Not Found");
276
+ return;
277
+ }
278
+ const { code, state: returnedState, error, error_description } = parsedUrl.query;
279
+ if (error) {
280
+ const errorMsg = String(error_description || error);
281
+ res.writeHead(400, { "Content-Type": "text/html" });
282
+ res.end(ERROR_HTML(errorMsg));
283
+ cleanup();
284
+ reject(new Error(`OAuth error: ${errorMsg}`));
285
+ return;
286
+ }
287
+ if (!code || !returnedState) {
288
+ res.writeHead(400, { "Content-Type": "text/html" });
289
+ res.end(ERROR_HTML("Missing code or state parameter"));
290
+ cleanup();
291
+ reject(new Error("Missing code or state parameter"));
292
+ return;
293
+ }
294
+ if (returnedState !== state) {
295
+ res.writeHead(400, { "Content-Type": "text/html" });
296
+ res.end(ERROR_HTML("Invalid state parameter"));
297
+ cleanup();
298
+ reject(new Error("Invalid state parameter (possible CSRF)"));
299
+ return;
300
+ }
301
+ res.writeHead(200, { "Content-Type": "text/html" });
302
+ res.end(SUCCESS_HTML);
303
+ try {
304
+ const tokenResponse = await exchangeCodeForToken(String(code), callbackUrl);
305
+ const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
306
+ storeCredentials({
307
+ accessToken: tokenResponse.access_token,
308
+ refreshToken: tokenResponse.refresh_token,
309
+ expiresAt
310
+ });
311
+ const user = tokenResponse.user || { id: "unknown", email: "unknown", name: "Unknown" };
312
+ storeUserInfo(user);
313
+ globalThis.setTimeout(() => {
314
+ cleanup();
315
+ resolve({ email: user.email, name: user.name });
316
+ }, 100);
317
+ } catch (err) {
318
+ cleanup();
319
+ reject(err);
320
+ }
321
+ };
322
+ server2 = createServer(handleRequest);
323
+ server2.on("error", (err) => {
324
+ cleanup();
325
+ reject(new Error(`OAuth server error: ${err.message}`));
326
+ });
327
+ server2.listen(port, "127.0.0.1", () => {
328
+ openBrowser(authUrl).catch(() => {
329
+ });
330
+ });
331
+ timeoutId = globalThis.setTimeout(() => {
332
+ cleanup();
333
+ reject(new Error("Authentication timed out. No callback received within 2 minutes."));
334
+ }, TIMEOUT_MS);
335
+ });
336
+ }
337
+
338
+ // src/tools/login.ts
339
+ var loginTool = {
340
+ name: "login",
341
+ description: "Authenticate with BragDuck. Opens the browser for sign-in/sign-up. Call this when other tools return an authentication error.",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ force: {
346
+ type: "boolean",
347
+ description: "Force re-authentication even if already logged in"
348
+ }
349
+ }
350
+ }
351
+ };
352
+ async function handleLogin(input) {
353
+ if (!input?.force) {
354
+ const existingToken = getAccessToken();
355
+ if (existingToken) {
356
+ return {
357
+ content: [{ type: "text", text: "Already authenticated with BragDuck." }]
358
+ };
359
+ }
360
+ }
361
+ try {
362
+ const user = await login();
363
+ return {
364
+ content: [{
365
+ type: "text",
366
+ text: `Authenticated as ${user.name} (${user.email}). You can now log brags and manage objectives.`
367
+ }]
368
+ };
369
+ } catch (error) {
370
+ return {
371
+ content: [{
372
+ type: "text",
373
+ text: `Authentication failed: ${error instanceof Error ? error.message : String(error)}`
374
+ }],
375
+ isError: true
376
+ };
377
+ }
378
+ }
379
+
380
+ // src/api-client.ts
381
+ var API_BASE_URL2 = process.env.BRAGDUCK_API_URL || "https://api.bragduck.com";
382
+ var DEBUG = !!process.env.BRAGDUCK_DEBUG;
383
+ var OAUTH_CLIENT_ID2 = "bragduck-cli";
384
+ var TOKEN_ENDPOINT = "/v1/auth/cli/token";
385
+ function debug(...args) {
386
+ if (DEBUG) console.error("[bragduck-mcp]", ...args);
387
+ }
388
+ async function refreshAccessToken() {
389
+ const refreshToken = getRefreshToken();
390
+ if (!refreshToken) return null;
391
+ try {
392
+ const response = await fetch(`${API_BASE_URL2}${TOKEN_ENDPOINT}`, {
393
+ method: "POST",
394
+ headers: { "Content-Type": "application/json" },
395
+ body: JSON.stringify({
396
+ refresh_token: refreshToken,
397
+ client_id: OAUTH_CLIENT_ID2,
398
+ grant_type: "refresh_token"
399
+ })
400
+ });
401
+ if (!response.ok) return null;
402
+ const data = await response.json();
403
+ const expiresAt = data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0;
404
+ storeCredentials({
405
+ accessToken: data.access_token,
406
+ refreshToken: data.refresh_token || refreshToken,
407
+ expiresAt
408
+ });
409
+ return data.access_token;
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+ async function apiRequest(endpoint, options = {}) {
415
+ const { method = "GET", body, params } = options;
416
+ let token = getAccessToken();
417
+ if (!token) {
418
+ throw new Error("NOT_AUTHENTICATED");
419
+ }
420
+ let url = `${API_BASE_URL2}${endpoint}`;
421
+ if (params) {
422
+ const searchParams = new URLSearchParams();
423
+ for (const [key, value] of Object.entries(params)) {
424
+ if (Array.isArray(value)) {
425
+ value.forEach((v) => searchParams.append(`${key}[]`, v));
426
+ } else {
427
+ searchParams.append(key, value);
428
+ }
429
+ }
430
+ url += `?${searchParams.toString()}`;
431
+ }
432
+ const headers = {
433
+ Authorization: `Bearer ${token}`,
434
+ "User-Agent": "BragDuck-MCP/1.0.0"
435
+ };
436
+ if (body) {
437
+ headers["Content-Type"] = "application/json";
438
+ }
439
+ const bodyString = body ? JSON.stringify(body) : void 0;
440
+ debug(`${method} ${url}`);
441
+ if (bodyString) debug("Request body:", bodyString);
442
+ let response = await fetch(url, {
443
+ method,
444
+ headers,
445
+ body: bodyString
446
+ });
447
+ debug(`Response: ${response.status} ${response.statusText}`);
448
+ if (response.status === 401) {
449
+ const newToken = await refreshAccessToken();
450
+ if (!newToken) {
451
+ throw new Error("TOKEN_EXPIRED");
452
+ }
453
+ headers.Authorization = `Bearer ${newToken}`;
454
+ response = await fetch(url, {
455
+ method,
456
+ headers,
457
+ body: bodyString
458
+ });
459
+ }
460
+ if (!response.ok) {
461
+ const text = await response.text().catch(() => "");
462
+ debug("Error response body:", text);
463
+ let msg = `API error: ${response.status} ${response.statusText}`;
464
+ try {
465
+ const errorData = JSON.parse(text);
466
+ msg = errorData.message || errorData.error || msg;
467
+ } catch {
468
+ if (text) msg = text;
469
+ }
470
+ throw new Error(msg);
471
+ }
472
+ return response.json();
473
+ }
474
+ async function getSubscriptionStatus() {
475
+ return apiRequest("/v1/subscriptions/status");
476
+ }
477
+ async function requirePaidPlan() {
478
+ try {
479
+ const { tier } = await getSubscriptionStatus();
480
+ if (tier === "FREE") {
481
+ return {
482
+ content: [
483
+ {
484
+ type: "text",
485
+ text: "BragDuck MCP requires a PLUS or PRO subscription.\n\nThe free plan does not include CLI or MCP access.\nUpgrade at: https://bragduck.com/app/settings/plans"
486
+ }
487
+ ],
488
+ isError: true
489
+ };
490
+ }
491
+ return null;
492
+ } catch {
493
+ return null;
494
+ }
495
+ }
496
+
497
+ // src/tools/log-brag.ts
498
+ var logBragTool = {
499
+ name: "log_brag",
500
+ description: "Persist a career achievement (brag) to BragDuck. Call this in the same response as the preview table \u2014 the tool permission prompt (Allow/Deny) that appears to the user IS the confirmation. Do not ask the user to type anything.",
501
+ inputSchema: {
502
+ type: "object",
503
+ properties: {
504
+ title: {
505
+ type: "string",
506
+ minLength: 10,
507
+ maxLength: 200,
508
+ description: "Short title of the achievement (must be 10-200 characters)"
509
+ },
510
+ description: {
511
+ type: "string",
512
+ minLength: 50,
513
+ maxLength: 2e3,
514
+ description: "Detailed description of what was accomplished and its impact (must be 50-2000 characters)"
515
+ },
516
+ tags: {
517
+ type: "array",
518
+ items: { type: "string", minLength: 2, maxLength: 30 },
519
+ maxItems: 10,
520
+ description: 'Tags to categorize the brag (e.g., "performance", "leadership", "backend"). Each tag must be 2-30 characters. Maximum 10 tags.'
521
+ },
522
+ date: {
523
+ type: "string",
524
+ description: "ISO datetime string for when this happened (defaults to now)"
525
+ },
526
+ impact_level: {
527
+ type: "number",
528
+ enum: [0, 1, 2, 3],
529
+ description: "Impact level of the achievement. 0 = none (personal learning, routine tasks). 1 = low (individual contributor impact, fixed small bug). 2 = medium (team-level impact, improved team process, led project). 3 = high (organization-level impact, reduced costs, improved key metrics)."
530
+ },
531
+ impact_description: {
532
+ type: "string",
533
+ description: 'Narrative description of the impact. Start with scope (Individual/Team/Organization/Company-wide), then include 1-3 measurable outcomes. Example: "Team impact: Reduced deploy time by 60%, unblocking 4 engineers"'
534
+ },
535
+ type: {
536
+ type: "string",
537
+ enum: [
538
+ "progress",
539
+ "achievement",
540
+ "recognition",
541
+ "support",
542
+ "code",
543
+ "leadership",
544
+ "learning",
545
+ "collaboration",
546
+ "challenge",
547
+ "innovation",
548
+ "meeting",
549
+ "bug",
550
+ "others"
551
+ ],
552
+ description: "Type of achievement. Choose the most appropriate category for this brag."
553
+ },
554
+ references: {
555
+ type: "array",
556
+ items: { type: "string", format: "uri" },
557
+ maxItems: 10,
558
+ description: 'URLs to reference materials for this achievement. Include any relevant links that provide context or proof: pull request URLs, Jira/Linear tickets, commit URLs, design docs, Notion pages, etc. Example: ["https://github.com/org/repo/pull/123", "https://jira.company.com/browse/PROJ-456"]'
559
+ },
560
+ orgId: {
561
+ type: "string",
562
+ description: "Organisation ID to associate the brag with"
563
+ },
564
+ agent: {
565
+ type: "string",
566
+ maxLength: 50,
567
+ description: 'Optional AI agent/client name for MCP-origin brags, e.g. "claude", "codex", "cursor", or "copilot".'
568
+ }
21
569
  },
22
- instructions: 'BragDuck tracks career achievements ("brags") that feed into periodic performance reports. ' +
23
- 'A strong brag needs: a concrete accomplishment, measurable or observable impact, and ideally a reference link. ' +
24
- '\n\n' +
25
- 'RESOLVING "THIS" IN "BRAG WITH THIS"\n' +
26
- 'When the user says "brag with this" (or "brag about this", "log this", etc.) without elaborating, ' +
27
- '"this" refers to the most recent work visible in the current session: ' +
28
- 'the latest git diff, the last commit message, the most recently discussed code change, or a PR just mentioned. ' +
29
- 'Look for that context first before asking anything. ' +
30
- 'Only start the interview (see below) if no such context exists in the session.\n' +
31
- '\n' +
32
- 'STEP 1 — ASSESS BRAGABILITY\n' +
33
- 'Before doing anything else, judge whether the subject is worth logging:\n' +
34
- '- NOT bragable: vague status updates, trivial tasks with no impact, ' +
35
- ' or things a reasonable person would not list on a performance review. ' +
36
- ' Tell the user directly that it is not bragable and explain why in one sentence. ' +
37
- ' Note that brags feed the AI report generator — weak entries dilute the signal.\n' +
38
- '- POTENTIALLY bragable but thin: start the interview (see below).\n' +
39
- '- Clearly bragable with full context: skip the interview, go straight to STEP 2.\n' +
40
- '\n' +
41
- 'THE INTERVIEW\n' +
42
- 'Conduct a short conversational interview to gather what you need. ' +
43
- 'Ask ONE question at a time — do not list multiple questions at once. ' +
44
- 'Each question should follow naturally from the previous answer, like a real conversation. ' +
45
- 'Suggested question order (adapt based on what you already know):\n' +
46
- ' 1. "What did you work on?" (skip if already known)\n' +
47
- ' 2. "What was the outcome or result?"\n' +
48
- ' 3. "Who was affected — just you, the team, or wider?"\n' +
49
- ' 4. "Do you have a link — PR, ticket, commit, or doc?" (skip if already have one)\n' +
50
- 'Stop the interview as soon as you have enough to write a strong brag. ' +
51
- 'Never ask more than 4 questions total.\n' +
52
- '\n' +
53
- 'STEP 2 — SELECT ORGANISATION\n' +
54
- 'Before composing the brag, determine which company it belongs to:\n' +
55
- '1. Call list_organisations. It returns the org list AND any saved preference for the current directory.\n' +
56
- '2. If a saved preference exists, use that orgId silently — do not ask the user.\n' +
57
- '3. If no saved preference, present the org list to the user and ask: "Which company does this belong to?" ' +
58
- ' Wait for their answer, then call save_org_preference with the chosen orgId and orgName.\n' +
59
- '4. If the user has only one organisation, select it automatically and save the preference.\n' +
60
- '5. If the user has no organisations, skip this step and proceed without an orgId.\n' +
61
- '\n' +
62
- 'STEP 3 — PREVIEW + LOG\n' +
63
- 'Compose the brag from all gathered context. ' +
64
- 'Do NOT call preview_brag. Instead, in your response text render the brag fields ' +
65
- 'as a markdown table with two columns (Field | Value), one row per field, in this order: ' +
66
- 'Title, Type, Impact Level, Impact Details, Description, Tags, References, Organisation. ' +
67
- 'In the very same response, after the table, call log_brag with those field values (including orgId). ' +
68
- 'The native tool-permission prompt (Allow / Deny) that appears below the table IS the ' +
69
- 'confirmation — do not add any text question. ' +
70
- 'If the user denies or asks for changes, adjust and repeat this step.\n' +
71
- '\n' +
72
- 'STEP 4 — DONE\n' +
73
- 'Once log_brag is allowed and succeeds, confirm with one short sentence.\n' +
74
- '\n' +
75
- 'QUICK BRAG (via /brag or raw text)\n' +
76
- 'When the user says "/brag" followed by text, or pastes a long block of text and asks to log it, ' +
77
- 'use the quick_brag tool instead of the interview flow. ' +
78
- 'quick_brag accepts raw text and uses AI to extract and structure it into a proper brag automatically. ' +
79
- 'Skip the interview, skip the preview table — just call quick_brag with the text.\n' +
80
- '\n' +
81
- 'INTERVIEW STORIES (log_story)\n' +
82
- 'When the user discusses interview prep, behavioral questions, or asks to save a STAR story, ' +
83
- 'use log_story instead of log_brag. Stories are structured with Situation/Task/Action/Result fields ' +
84
- 'and competency tags. Follow the same org-selection flow (list_organisations → saved preference or ask). ' +
85
- 'Preview the story as a markdown table (Field | Value) before calling log_story in the same response.\n' +
86
- '\n' +
87
- 'GENERAL RULES\n' +
88
- '- Never fabricate details, metrics, or links. If unsure, ask.\n' +
89
- '- "Is this bragable?" deserves a direct honest answer — not a tool call.\n' +
90
- '- Call login first if any tool returns an authentication error.',
91
- });
92
- server.setRequestHandler(ListToolsRequestSchema, async () => {
570
+ required: ["title", "description", "type", "impact_level", "impact_description", "references"]
571
+ }
572
+ };
573
+ function toAgentExternalType(agent) {
574
+ if (!agent) return void 0;
575
+ const normalized = agent.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "");
576
+ if (normalized.length === 0) return void 0;
577
+ return `agent:${normalized.slice(0, 50)}`;
578
+ }
579
+ async function handleLogBrag(input) {
580
+ const planError = await requirePaidPlan();
581
+ if (planError) return planError;
582
+ try {
583
+ const brag = {
584
+ title: input.title,
585
+ message: input.description,
586
+ description: input.description,
587
+ tags: input.tags || [],
588
+ date: input.date || (/* @__PURE__ */ new Date()).toISOString(),
589
+ visibility: "private"
590
+ };
591
+ if (input.impact_level !== void 0) brag.impactLevel = input.impact_level;
592
+ if (input.impact_description) brag.impact_description = input.impact_description;
593
+ if (input.type) brag.typeKey = input.type;
594
+ if (input.references && input.references.length > 0) brag.attachments = input.references;
595
+ if (input.orgId) brag.orgId = input.orgId;
596
+ const agentExternalType = toAgentExternalType(input.agent);
597
+ if (agentExternalType) brag.externalType = agentExternalType;
598
+ const response = await apiRequest("/v1/brags", {
599
+ method: "POST",
600
+ body: { brags: [brag], source: "mcp" }
601
+ });
602
+ if (response.brags && response.brags.length > 0) {
603
+ const created = response.brags[0];
604
+ const lines = [`Brag logged: "${created.title}"
605
+ View it at: ${created.url}`];
606
+ if (created.keyResultIds && created.keyResultIds.length > 0) {
607
+ lines.push(`Automatically linked to key result: ${created.keyResultIds[0]}`);
608
+ }
609
+ return {
610
+ content: [{
611
+ type: "text",
612
+ text: lines.join("\n")
613
+ }]
614
+ };
615
+ }
616
+ const firstError = response.errors && response.errors.length > 0 ? response.errors[0].message : "Server returned no brag and no error detail";
617
+ return {
618
+ content: [{
619
+ type: "text",
620
+ text: `Failed to create brag: ${firstError}`
621
+ }],
622
+ isError: true
623
+ };
624
+ } catch (error) {
625
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
626
+ return {
627
+ content: [{
628
+ type: "text",
629
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
630
+ }],
631
+ isError: true
632
+ };
633
+ }
634
+ return {
635
+ content: [{
636
+ type: "text",
637
+ text: `Failed to create brag: ${error instanceof Error ? error.message : String(error)}`
638
+ }],
639
+ isError: true
640
+ };
641
+ }
642
+ }
643
+
644
+ // src/tools/quick-brag.ts
645
+ var quickBragTool = {
646
+ name: "quick_brag",
647
+ description: 'Log a brag from raw text. Accepts free-form text (a sentence, paragraph, or paste from a message/PR/doc) and uses AI to extract and structure it into a proper brag. The AI will parse the text into a title, description, type, impact, and tags, then log it. Use this when the user says "/brag" followed by text, or pastes something and asks to log it as a brag.',
648
+ inputSchema: {
649
+ type: "object",
650
+ properties: {
651
+ text: {
652
+ type: "string",
653
+ minLength: 10,
654
+ maxLength: 5e3,
655
+ description: "Raw text describing the achievement. Can be a sentence, paragraph, paste from a PR description, Slack message, commit message, or any free-form text. The AI will extract brag fields from it."
656
+ },
657
+ orgId: {
658
+ type: "string",
659
+ description: "Organisation ID to associate the brag with (optional)"
660
+ },
661
+ agent: {
662
+ type: "string",
663
+ maxLength: 50,
664
+ description: 'Optional AI agent/client name, e.g. "claude", "cursor", "copilot".'
665
+ }
666
+ },
667
+ required: ["text"]
668
+ }
669
+ };
670
+ function toAgentExternalType2(agent) {
671
+ if (!agent) return void 0;
672
+ const normalized = agent.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "");
673
+ if (normalized.length === 0) return void 0;
674
+ return `agent:${normalized.slice(0, 50)}`;
675
+ }
676
+ async function handleQuickBrag(input) {
677
+ const planError = await requirePaidPlan();
678
+ if (planError) return planError;
679
+ try {
680
+ const refined = await apiRequest("/v1/brags/refine", {
681
+ method: "POST",
682
+ body: { brags: [{ text: input.text }] }
683
+ });
684
+ if (!refined.refined_brags || refined.refined_brags.length === 0) {
685
+ return {
686
+ content: [{
687
+ type: "text",
688
+ text: "Could not extract a brag from the provided text. Try adding more detail about what was accomplished and its impact."
689
+ }],
690
+ isError: true
691
+ };
692
+ }
693
+ const extracted = refined.refined_brags[0];
694
+ const brag = {
695
+ title: extracted.refined_title,
696
+ description: extracted.refined_description,
697
+ tags: extracted.suggested_tags || [],
698
+ date: (/* @__PURE__ */ new Date()).toISOString(),
699
+ visibility: "private",
700
+ impactLevel: extracted.suggested_impactLevel,
701
+ impact_description: extracted.impact_description || "",
702
+ typeKey: extracted.suggested_type_key
703
+ };
704
+ const allLinks = [
705
+ ...extracted.extracted_links || [],
706
+ ...extracted.attachments || []
707
+ ].filter(Boolean);
708
+ if (allLinks.length > 0) brag.attachments = allLinks;
709
+ if (input.orgId) brag.orgId = input.orgId;
710
+ const agentExternalType = toAgentExternalType2(input.agent);
711
+ if (agentExternalType) brag.externalType = agentExternalType;
712
+ const response = await apiRequest("/v1/brags", {
713
+ method: "POST",
714
+ body: { brags: [brag], source: "mcp" }
715
+ });
716
+ if (response.errors && response.errors.length > 0) {
717
+ const errorMessages = response.errors.map((e) => e.message).join("; ");
718
+ return {
719
+ content: [{
720
+ type: "text",
721
+ text: `Failed to create brag: ${errorMessages}`
722
+ }],
723
+ isError: true
724
+ };
725
+ }
726
+ if (response.brags && response.brags.length > 0) {
727
+ const created = response.brags[0];
728
+ const lines = [
729
+ `Brag logged: "${created.title}"`,
730
+ `${created.url}`
731
+ ];
732
+ if (created.keyResultIds && created.keyResultIds.length > 0) {
733
+ lines.push(`Automatically linked to key result: ${created.keyResultIds[0]}`);
734
+ }
735
+ return {
736
+ content: [{
737
+ type: "text",
738
+ text: lines.join("\n")
739
+ }]
740
+ };
741
+ }
742
+ return {
743
+ content: [{
744
+ type: "text",
745
+ text: "Server returned no brag and no error detail"
746
+ }],
747
+ isError: true
748
+ };
749
+ } catch (error) {
750
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
751
+ return {
752
+ content: [{
753
+ type: "text",
754
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
755
+ }],
756
+ isError: true
757
+ };
758
+ }
759
+ return {
760
+ content: [{
761
+ type: "text",
762
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
763
+ }],
764
+ isError: true
765
+ };
766
+ }
767
+ }
768
+
769
+ // src/tools/preview-brag.ts
770
+ var IMPACT_LABELS = {
771
+ 0: "None (personal learning / routine)",
772
+ 1: "Low (individual contributor impact)",
773
+ 2: "Medium (team-level impact)",
774
+ 3: "High (organisation-level impact)"
775
+ };
776
+ var TYPE_EMOJIS = {
777
+ progress: "\u{1F4C8}",
778
+ achievement: "\u{1F3C6}",
779
+ recognition: "\u{1F31F}",
780
+ support: "\u{1F91D}",
781
+ code: "\u{1F4BB}",
782
+ leadership: "\u{1F9ED}",
783
+ learning: "\u{1F4DA}",
784
+ collaboration: "\u{1F465}",
785
+ challenge: "\u2694\uFE0F",
786
+ innovation: "\u{1F4A1}",
787
+ meeting: "\u{1F5E3}\uFE0F",
788
+ bug: "\u{1F41B}",
789
+ others: "\u{1F4CC}"
790
+ };
791
+ function handlePreviewBrag(input) {
792
+ const emoji = TYPE_EMOJIS[input.type] ?? "\u{1F4CC}";
793
+ const impactLabel = IMPACT_LABELS[input.impact_level] ?? String(input.impact_level);
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: JSON.stringify({
799
+ title: input.title,
800
+ type: `${emoji} ${input.type}`,
801
+ impact_level: impactLabel,
802
+ impact_details: input.impact_description,
803
+ description: input.description,
804
+ tags: input.tags && input.tags.length > 0 ? input.tags.join(", ") : "\u2014",
805
+ references: input.references && input.references.length > 0 ? input.references.join(", ") : "\u2014"
806
+ })
807
+ }
808
+ ]
809
+ };
810
+ }
811
+
812
+ // src/tools/list-brags.ts
813
+ var listBragsTool = {
814
+ name: "list_brags",
815
+ description: "List recent brags (career achievements) from BragDuck. Use this to review past accomplishments, find brags to link to keyResults, or check recent entries.",
816
+ inputSchema: {
817
+ type: "object",
818
+ properties: {
819
+ limit: {
820
+ type: "number",
821
+ description: "Number of brags to return (default: 10, max: 100)"
822
+ },
823
+ search: {
824
+ type: "string",
825
+ description: "Search term to filter brags by title or description"
826
+ },
827
+ tags: {
828
+ type: "array",
829
+ items: { type: "string" },
830
+ description: "Filter brags by tags"
831
+ },
832
+ date_from: {
833
+ type: "string",
834
+ description: "Filter brags from this date (YYYY-MM-DD)"
835
+ },
836
+ date_to: {
837
+ type: "string",
838
+ description: "Filter brags up to this date (YYYY-MM-DD)"
839
+ },
840
+ sort: {
841
+ type: "string",
842
+ enum: ["date_asc", "date_desc", "title_asc", "title_desc", "created_asc", "created_desc"],
843
+ description: "Sort order (default: date_desc)"
844
+ }
845
+ }
846
+ }
847
+ };
848
+ var IMPACT_LABELS2 = {
849
+ 0: "None",
850
+ 1: "Low",
851
+ 2: "Medium",
852
+ 3: "High"
853
+ };
854
+ async function handleListBrags(input) {
855
+ const planError = await requirePaidPlan();
856
+ if (planError) return planError;
857
+ try {
858
+ const params = {
859
+ limit: String(input.limit || 10),
860
+ offset: "0"
861
+ };
862
+ if (input.search) params.search = input.search;
863
+ if (input.tags && input.tags.length > 0) params["tags[]"] = input.tags;
864
+ if (input.date_from) params.date_from = input.date_from;
865
+ if (input.date_to) params.date_to = input.date_to;
866
+ if (input.sort) params.sort = input.sort;
867
+ const response = await apiRequest("/v1/brags", { params });
868
+ if (response.brags.length === 0) {
869
+ return {
870
+ content: [{ type: "text", text: "No brags found." }]
871
+ };
872
+ }
873
+ const lines = response.brags.map((brag) => {
874
+ const tags = brag.tags.length > 0 ? ` [${brag.tags.join(", ")}]` : "";
875
+ const date = new Date(brag.date).toLocaleDateString();
876
+ const impact = `Impact: ${IMPACT_LABELS2[brag.impactLevel ?? 0] ?? (brag.impactLevel ?? 0)}`;
877
+ const type = brag.type ? ` | Type: ${brag.type}` : "";
878
+ const keyResultNote = brag.keyResultIds?.length > 0 ? `
879
+ Linked to key result: ${brag.keyResultIds[0]}` : "";
880
+ const impactDesc = brag.impactDescription ? `
881
+ Impact: ${brag.impactDescription}` : "";
882
+ return `- **${brag.title}** (ID: ${brag.id}, ${date})${tags}
883
+ ${impact}${type}` + impactDesc + keyResultNote + `
884
+ ${brag.description.slice(0, 150)}${brag.description.length > 150 ? "..." : ""}
885
+ View: ${brag.url}`;
886
+ });
887
+ const { total, has_more } = response.pagination;
888
+ const summary = `Found ${total} brags (showing ${response.brags.length}${has_more ? ", more available" : ""}):`;
889
+ return {
890
+ content: [{ type: "text", text: `${summary}
891
+
892
+ ${lines.join("\n\n")}` }]
893
+ };
894
+ } catch (error) {
895
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
896
+ return {
897
+ content: [{
898
+ type: "text",
899
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
900
+ }],
901
+ isError: true
902
+ };
903
+ }
904
+ return {
905
+ content: [{
906
+ type: "text",
907
+ text: `Failed to list brags: ${error instanceof Error ? error.message : String(error)}`
908
+ }],
909
+ isError: true
910
+ };
911
+ }
912
+ }
913
+
914
+ // src/tools/list-objectives.ts
915
+ var listObjectivesTool = {
916
+ name: "list_objectives",
917
+ description: "List OKRs (Objectives and Key Results) from BragDuck. Use this to see current keyResults and their IDs, which can be used to link brags to specific keyResults.",
918
+ inputSchema: {
919
+ type: "object",
920
+ properties: {}
921
+ }
922
+ };
923
+ async function handleListObjectives() {
924
+ const planError = await requirePaidPlan();
925
+ if (planError) return planError;
926
+ try {
927
+ const userInfo = getUserInfo();
928
+ if (!userInfo) {
929
+ return {
930
+ content: [{
931
+ type: "text",
932
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
933
+ }],
934
+ isError: true
935
+ };
936
+ }
937
+ const response = await apiRequest(
938
+ `/v1/users/${userInfo.id}/objectives`
939
+ );
940
+ if (response.items.length === 0) {
941
+ return {
942
+ content: [{ type: "text", text: "No objectives found." }]
943
+ };
944
+ }
945
+ const lines = response.items.map((obj) => {
946
+ const keyResults = obj.keyResults.map((g) => ` - ${g.label} (KeyResult ID: ${g.id}, status: ${g.status})`).join("\n");
947
+ const category = obj.category ? ` [${obj.category}]` : "";
948
+ return `- **${obj.title}** (ID: ${obj.id}, status: ${obj.status})${category}
949
+ ${keyResults || " (no keyResults)"}`;
950
+ });
951
+ return {
952
+ content: [{
953
+ type: "text",
954
+ text: `Found ${response.total} objectives:
955
+
956
+ ${lines.join("\n\n")}`
957
+ }]
958
+ };
959
+ } catch (error) {
960
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
961
+ return {
962
+ content: [{
963
+ type: "text",
964
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
965
+ }],
966
+ isError: true
967
+ };
968
+ }
969
+ return {
970
+ content: [{
971
+ type: "text",
972
+ text: `Failed to list objectives: ${error instanceof Error ? error.message : String(error)}`
973
+ }],
974
+ isError: true
975
+ };
976
+ }
977
+ }
978
+
979
+ // src/tools/link-brag-to-key-result.ts
980
+ var linkBragToKeyResultTool = {
981
+ name: "link_brag_to_key_result",
982
+ description: "Add a keyResult/key-result to a brag (supports multiple keyResults per brag). Use list_objectives to find keyResult IDs and list_brags to find brag IDs.",
983
+ inputSchema: {
984
+ type: "object",
985
+ properties: {
986
+ bragId: {
987
+ type: "string",
988
+ description: "The ID of the brag to update"
989
+ },
990
+ keyResultId: {
991
+ type: "string",
992
+ description: "The ID of the keyResult to link the brag to"
993
+ }
994
+ },
995
+ required: ["bragId", "keyResultId"]
996
+ }
997
+ };
998
+ async function handleLinkBragToKeyResult(input) {
999
+ const planError = await requirePaidPlan();
1000
+ if (planError) return planError;
1001
+ try {
1002
+ const userInfo = getUserInfo();
1003
+ if (!userInfo) {
1004
+ return {
1005
+ content: [{
1006
+ type: "text",
1007
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1008
+ }],
1009
+ isError: true
1010
+ };
1011
+ }
1012
+ const currentBrag = await apiRequest(
1013
+ `/v1/users/${userInfo.id}/brags/${input.bragId}`
1014
+ );
1015
+ const existingKeyResultIds = currentBrag.keyResultIds || [];
1016
+ if (existingKeyResultIds.includes(input.keyResultId)) {
1017
+ return {
1018
+ content: [{
1019
+ type: "text",
1020
+ text: `Brag ${input.bragId} is already linked to keyResult ${input.keyResultId}.`
1021
+ }]
1022
+ };
1023
+ }
1024
+ const updatedKeyResultIds = [...existingKeyResultIds, input.keyResultId];
1025
+ await apiRequest(`/v1/users/${userInfo.id}/brags/${input.bragId}`, {
1026
+ method: "PATCH",
1027
+ body: { keyResultIds: updatedKeyResultIds }
1028
+ });
1029
+ return {
1030
+ content: [{
1031
+ type: "text",
1032
+ text: `KeyResult ${input.keyResultId} added to brag ${input.bragId} (now linked to ${updatedKeyResultIds.length} keyResult${updatedKeyResultIds.length === 1 ? "" : "s"}).`
1033
+ }]
1034
+ };
1035
+ } catch (error) {
1036
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
1037
+ return {
1038
+ content: [{
1039
+ type: "text",
1040
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1041
+ }],
1042
+ isError: true
1043
+ };
1044
+ }
1045
+ return {
1046
+ content: [{
1047
+ type: "text",
1048
+ text: `Failed to link brag to keyResult: ${error instanceof Error ? error.message : String(error)}`
1049
+ }],
1050
+ isError: true
1051
+ };
1052
+ }
1053
+ }
1054
+
1055
+ // src/org-preference.ts
1056
+ import Conf2 from "conf";
1057
+ var APP_NAME2 = "bragduck";
1058
+ function getConfig() {
1059
+ return new Conf2({ projectName: APP_NAME2 });
1060
+ }
1061
+ function getOrgPreference(cwd) {
1062
+ try {
1063
+ const config = getConfig();
1064
+ const prefs = config.get("orgPreferences") || {};
1065
+ return prefs[cwd] || null;
1066
+ } catch {
1067
+ return null;
1068
+ }
1069
+ }
1070
+ function saveOrgPreference(cwd, orgId, orgName) {
1071
+ const config = getConfig();
1072
+ const prefs = config.get("orgPreferences") || {};
1073
+ prefs[cwd] = { orgId, orgName };
1074
+ config.set("orgPreferences", prefs);
1075
+ }
1076
+
1077
+ // src/tools/list-organisations.ts
1078
+ var listOrganisationsTool = {
1079
+ name: "list_organisations",
1080
+ description: "List the user's organisations (companies) from BragDuck. Also returns the saved organisation preference for the current working directory, if any. Use this before logging a brag to determine which company to associate it with.",
1081
+ inputSchema: {
1082
+ type: "object",
1083
+ properties: {}
1084
+ }
1085
+ };
1086
+ async function handleListOrganisations() {
1087
+ const planError = await requirePaidPlan();
1088
+ if (planError) return planError;
1089
+ try {
1090
+ const userInfo = getUserInfo();
1091
+ if (!userInfo) {
1092
+ return {
1093
+ content: [{
1094
+ type: "text",
1095
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1096
+ }],
1097
+ isError: true
1098
+ };
1099
+ }
1100
+ const response = await apiRequest(
1101
+ `/v1/users/${userInfo.id}/organisations`
1102
+ );
1103
+ if (response.items.length === 0) {
1104
+ return {
1105
+ content: [{
1106
+ type: "text",
1107
+ text: "No organisations found. You can add companies in the BragDuck web app."
1108
+ }]
1109
+ };
1110
+ }
1111
+ const cwd = process.cwd();
1112
+ const saved = getOrgPreference(cwd);
1113
+ const lines = response.items.map((org) => {
1114
+ const status = org.current ? "current" : "former";
1115
+ return `- **${org.name}** (ID: ${org.id}, role: ${org.role}, ${status})`;
1116
+ });
1117
+ const savedLine = saved ? `
1118
+ Saved preference for this directory: **${saved.orgName}** (ID: ${saved.orgId})` : "\nNo saved preference for this directory.";
1119
+ return {
1120
+ content: [{
1121
+ type: "text",
1122
+ text: `Found ${response.items.length} organisation(s):
1123
+
1124
+ ${lines.join("\n")}${savedLine}`
1125
+ }]
1126
+ };
1127
+ } catch (error) {
1128
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
1129
+ return {
1130
+ content: [{
1131
+ type: "text",
1132
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1133
+ }],
1134
+ isError: true
1135
+ };
1136
+ }
1137
+ return {
1138
+ content: [{
1139
+ type: "text",
1140
+ text: `Failed to list organisations: ${error instanceof Error ? error.message : String(error)}`
1141
+ }],
1142
+ isError: true
1143
+ };
1144
+ }
1145
+ }
1146
+
1147
+ // src/tools/save-org-preference.ts
1148
+ var saveOrgPreferenceTool = {
1149
+ name: "save_org_preference",
1150
+ description: "Save the organisation preference for the current working directory. Call this after the user selects which company a brag belongs to, so the same choice is remembered for future brags in this directory.",
1151
+ inputSchema: {
1152
+ type: "object",
1153
+ properties: {
1154
+ orgId: {
1155
+ type: "string",
1156
+ description: "The organisation ID to save as the preference for this directory"
1157
+ },
1158
+ orgName: {
1159
+ type: "string",
1160
+ description: "The organisation name (for display purposes)"
1161
+ }
1162
+ },
1163
+ required: ["orgId", "orgName"]
1164
+ }
1165
+ };
1166
+ async function handleSaveOrgPreference(input) {
1167
+ const planError = await requirePaidPlan();
1168
+ if (planError) return planError;
1169
+ try {
1170
+ const cwd = process.cwd();
1171
+ saveOrgPreference(cwd, input.orgId, input.orgName);
1172
+ return {
1173
+ content: [{
1174
+ type: "text",
1175
+ text: `Saved: brags in this directory will be associated with **${input.orgName}**.`
1176
+ }]
1177
+ };
1178
+ } catch (error) {
1179
+ return {
1180
+ content: [{
1181
+ type: "text",
1182
+ text: `Failed to save preference: ${error instanceof Error ? error.message : String(error)}`
1183
+ }],
1184
+ isError: true
1185
+ };
1186
+ }
1187
+ }
1188
+
1189
+ // src/tools/log-story.ts
1190
+ var logStoryTool = {
1191
+ name: "log_story",
1192
+ description: "Save a STAR-formatted interview story to BragDuck. Use this when the user discusses interview prep, behavioral questions, or asks to save a story. Each story has Situation, Task, Action, Result fields plus competency tags. Call this in the same response as a preview \u2014 the tool permission prompt (Allow/Deny) that appears to the user IS the confirmation.",
1193
+ inputSchema: {
1194
+ type: "object",
1195
+ properties: {
1196
+ title: {
1197
+ type: "string",
1198
+ minLength: 5,
1199
+ maxLength: 200,
1200
+ description: 'Short title for the story (e.g., "Led migration to microservices")'
1201
+ },
1202
+ situation: {
1203
+ type: "string",
1204
+ minLength: 10,
1205
+ description: "The context and background \u2014 what was happening, what was the environment"
1206
+ },
1207
+ task: {
1208
+ type: "string",
1209
+ minLength: 10,
1210
+ description: "What was your specific responsibility or challenge"
1211
+ },
1212
+ action: {
1213
+ type: "string",
1214
+ minLength: 10,
1215
+ description: "What you specifically did \u2014 the steps you took"
1216
+ },
1217
+ result: {
1218
+ type: "string",
1219
+ minLength: 10,
1220
+ description: "The outcome \u2014 measurable impact, what changed, what you learned"
1221
+ },
1222
+ competencies: {
1223
+ type: "array",
1224
+ items: { type: "string", minLength: 2, maxLength: 50 },
1225
+ maxItems: 20,
1226
+ description: 'Skills or competencies this story demonstrates (e.g., "leadership", "system design", "conflict resolution")'
1227
+ },
1228
+ bragIds: {
1229
+ type: "array",
1230
+ items: { type: "string" },
1231
+ maxItems: 20,
1232
+ description: "IDs of BragDuck brags that back this story"
1233
+ },
1234
+ orgId: {
1235
+ type: "string",
1236
+ description: "Organisation ID to associate the story with"
1237
+ },
1238
+ category: {
1239
+ type: "string",
1240
+ description: "Interview category this story demonstrates (e.g. leadership, technical_excellence, problem_solving, innovation, teamwork, communication, project_delivery, customer_impact, mentorship, process_improvement)"
1241
+ }
1242
+ },
1243
+ required: ["title", "situation", "task", "action", "result"]
1244
+ }
1245
+ };
1246
+ async function handleLogStory(input) {
1247
+ const planError = await requirePaidPlan();
1248
+ if (planError) return planError;
1249
+ try {
1250
+ const userInfo = getUserInfo();
1251
+ if (!userInfo) {
1252
+ return {
1253
+ content: [{
1254
+ type: "text",
1255
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1256
+ }],
1257
+ isError: true
1258
+ };
1259
+ }
1260
+ const body = {
1261
+ title: input.title,
1262
+ situation: input.situation,
1263
+ task: input.task,
1264
+ action: input.action,
1265
+ result: input.result,
1266
+ competencies: input.competencies || [],
1267
+ bragIds: input.bragIds || [],
1268
+ source: "mcp"
1269
+ };
1270
+ if (input.orgId) body.orgId = input.orgId;
1271
+ if (input.category) body.category = input.category;
1272
+ const story = await apiRequest(
1273
+ `/v1/users/${userInfo.id}/stories`,
1274
+ { method: "POST", body }
1275
+ );
1276
+ const competencyLine = story.competencies.length > 0 ? `
1277
+ Competencies: ${story.competencies.join(", ")}` : "";
1278
+ return {
1279
+ content: [{
1280
+ type: "text",
1281
+ text: `Story saved: "${story.title}"${competencyLine}`
1282
+ }]
1283
+ };
1284
+ } catch (error) {
1285
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
1286
+ return {
1287
+ content: [{
1288
+ type: "text",
1289
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1290
+ }],
1291
+ isError: true
1292
+ };
1293
+ }
93
1294
  return {
94
- tools: [loginTool, logBragTool, quickBragTool, listBragsTool, listObjectivesTool, linkBragToKeyResultTool, listOrganisationsTool, saveOrgPreferenceTool, logStoryTool],
1295
+ content: [{
1296
+ type: "text",
1297
+ text: `Failed to save story: ${error instanceof Error ? error.message : String(error)}`
1298
+ }],
1299
+ isError: true
95
1300
  };
1301
+ }
1302
+ }
1303
+
1304
+ // ../server/src/utils/subscription/feature-config.ts
1305
+ var ORG_TEAM_ROLE_FEATURES = [
1306
+ "org_access" /* ORG_ACCESS */,
1307
+ "org_mutate_base" /* ORG_MUTATE_BASE */,
1308
+ "org_admin_access" /* ORG_ADMIN_ACCESS */,
1309
+ "org_owner_access" /* ORG_OWNER_ACCESS */,
1310
+ "org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */,
1311
+ "manager_tier_access" /* MANAGER_TIER_ACCESS */,
1312
+ "team_manage" /* TEAM_MANAGE */,
1313
+ "team_owner_actions" /* TEAM_OWNER_ACTIONS */,
1314
+ "team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
1315
+ "org_members_manage" /* ORG_MEMBERS_MANAGE */,
1316
+ "org_invites_manage" /* ORG_INVITES_MANAGE */,
1317
+ "org_teams_manage" /* ORG_TEAMS_MANAGE */,
1318
+ "org_growth_view" /* ORG_GROWTH_VIEW */,
1319
+ "org_career_metrics_manage" /* ORG_CAREER_METRICS_MANAGE */,
1320
+ "org_cycles_manage" /* ORG_CYCLES_MANAGE */,
1321
+ "org_stats_view" /* ORG_STATS_VIEW */,
1322
+ "org_audit_log_view" /* ORG_AUDIT_LOG_VIEW */,
1323
+ "org_billing_manage" /* ORG_BILLING_MANAGE */,
1324
+ "org_notifications_manage" /* ORG_NOTIFICATIONS_MANAGE */
1325
+ ];
1326
+ var DISABLED_ORG_TEAM_FEATURES = Object.fromEntries(
1327
+ ORG_TEAM_ROLE_FEATURES.map((f) => [f, { enabled: false }])
1328
+ );
1329
+ var UNIVERSAL_FEATURES = [
1330
+ "brags_create" /* BRAGS_CREATE */,
1331
+ "brags_read" /* BRAGS_READ */,
1332
+ "brags_update" /* BRAGS_UPDATE */,
1333
+ "brags_delete" /* BRAGS_DELETE */,
1334
+ "stories_create" /* STORIES_CREATE */,
1335
+ "stories_read" /* STORIES_READ */,
1336
+ "stories_update" /* STORIES_UPDATE */,
1337
+ "stories_delete" /* STORIES_DELETE */,
1338
+ "brag_comments_create" /* BRAG_COMMENTS_CREATE */,
1339
+ "brag_comments_read" /* BRAG_COMMENTS_READ */,
1340
+ "brag_comments_update" /* BRAG_COMMENTS_UPDATE */,
1341
+ "brag_comments_delete" /* BRAG_COMMENTS_DELETE */,
1342
+ "brag_types_create" /* BRAG_TYPES_CREATE */,
1343
+ "brag_types_read" /* BRAG_TYPES_READ */,
1344
+ "brag_types_update" /* BRAG_TYPES_UPDATE */,
1345
+ "objectives_read" /* OBJECTIVES_READ */,
1346
+ "objectives_update" /* OBJECTIVES_UPDATE */,
1347
+ "objectives_delete" /* OBJECTIVES_DELETE */,
1348
+ "objectives_key_results_create" /* OBJECTIVES_KEY_RESULTS_CREATE */,
1349
+ "objectives_key_results_update" /* OBJECTIVES_KEY_RESULTS_UPDATE */,
1350
+ "objectives_key_results_delete" /* OBJECTIVES_KEY_RESULTS_DELETE */,
1351
+ "reports_read" /* REPORTS_READ */,
1352
+ "reports_update" /* REPORTS_UPDATE */,
1353
+ "reports_delete" /* REPORTS_DELETE */,
1354
+ "shares_read" /* SHARES_READ */,
1355
+ "shares_delete" /* SHARES_DELETE */,
1356
+ "fields_read" /* FIELDS_READ */,
1357
+ "user_emails_read" /* USER_EMAILS_READ */,
1358
+ "notifications_preferences_read" /* NOTIFICATIONS_PREFERENCES_READ */,
1359
+ "notifications_preferences_update" /* NOTIFICATIONS_PREFERENCES_UPDATE */,
1360
+ "security_info_read" /* SECURITY_INFO_READ */,
1361
+ "data_access_log_read" /* DATA_ACCESS_LOG_READ */,
1362
+ "users_read" /* USERS_READ */,
1363
+ "users_update" /* USERS_UPDATE */,
1364
+ "users_delete" /* USERS_DELETE */,
1365
+ "badges_read" /* BADGES_READ */,
1366
+ "user_teams_accept_invite" /* USER_TEAMS_ACCEPT_INVITE */,
1367
+ "user_teams_reject_invite" /* USER_TEAMS_REJECT_INVITE */,
1368
+ "page_dashboard_view" /* PAGE_DASHBOARD_VIEW */,
1369
+ "page_brags_view" /* PAGE_BRAGS_VIEW */,
1370
+ "page_stories_view" /* PAGE_STORIES_VIEW */,
1371
+ "page_okrs_view" /* PAGE_OKRS_VIEW */,
1372
+ "page_reports_view" /* PAGE_REPORTS_VIEW */,
1373
+ "page_files_view" /* PAGE_FILES_VIEW */,
1374
+ "page_settings_view" /* PAGE_SETTINGS_VIEW */,
1375
+ "page_integrations_view" /* PAGE_INTEGRATIONS_VIEW */,
1376
+ "settings_tab_account_view" /* SETTINGS_TAB_ACCOUNT_VIEW */,
1377
+ "settings_tab_preferences_view" /* SETTINGS_TAB_PREFERENCES_VIEW */,
1378
+ "settings_tab_notifications_view" /* SETTINGS_TAB_NOTIFICATIONS_VIEW */,
1379
+ "settings_tab_billing_view" /* SETTINGS_TAB_BILLING_VIEW */,
1380
+ "settings_tab_org_view" /* SETTINGS_TAB_ORG_VIEW */,
1381
+ "settings_tab_plans_view" /* SETTINGS_TAB_PLANS_VIEW */
1382
+ ];
1383
+ var PLUS_GATED_FEATURES = [
1384
+ "brags_export" /* BRAGS_EXPORT */,
1385
+ "brags_import" /* BRAGS_IMPORT */,
1386
+ "reports_csv_export" /* REPORTS_CSV_EXPORT */,
1387
+ "llm_keys_create" /* LLM_KEYS_CREATE */,
1388
+ "llm_keys_read" /* LLM_KEYS_READ */,
1389
+ "llm_keys_update" /* LLM_KEYS_UPDATE */,
1390
+ "llm_keys_delete" /* LLM_KEYS_DELETE */,
1391
+ "llm_keys_validate" /* LLM_KEYS_VALIDATE */,
1392
+ "llm_settings_read" /* LLM_SETTINGS_READ */,
1393
+ "llm_settings_update" /* LLM_SETTINGS_UPDATE */,
1394
+ "user_settings_openai_key_manage" /* USER_SETTINGS_OPENAI_KEY_MANAGE */,
1395
+ "notifications_settings_read" /* NOTIFICATIONS_SETTINGS_READ */,
1396
+ "notifications_settings_update" /* NOTIFICATIONS_SETTINGS_UPDATE */,
1397
+ "cli_brags_create" /* CLI_BRAGS_CREATE */,
1398
+ "cli_brags_read" /* CLI_BRAGS_READ */,
1399
+ "cli_brags_refine" /* CLI_BRAGS_REFINE */,
1400
+ "commits_refine" /* COMMITS_REFINE */,
1401
+ "settings_tab_ai_keys_view" /* SETTINGS_TAB_AI_KEYS_VIEW */,
1402
+ "ask_ducky" /* ASK_DUCKY */
1403
+ ];
1404
+ var PRO_GATED_FEATURES = [
1405
+ "user_emails_update" /* USER_EMAILS_UPDATE */,
1406
+ "user_emails_delete" /* USER_EMAILS_DELETE */,
1407
+ "user_emails_verify" /* USER_EMAILS_VERIFY */,
1408
+ "badges_read_others" /* BADGES_READ_OTHERS */,
1409
+ "page_practice_view" /* PAGE_PRACTICE_VIEW */,
1410
+ "page_resume_view" /* PAGE_RESUME_VIEW */
1411
+ ];
1412
+ var TEAM_GATED_FEATURES = [
1413
+ "evaluation_comments_create" /* EVALUATION_COMMENTS_CREATE */,
1414
+ "evaluation_comments_read" /* EVALUATION_COMMENTS_READ */,
1415
+ "peer_reviews_view" /* PEER_REVIEWS_VIEW */,
1416
+ "peer_reviews_accept" /* PEER_REVIEWS_ACCEPT */,
1417
+ "peer_reviews_submit" /* PEER_REVIEWS_SUBMIT */,
1418
+ "user_teams_view" /* USER_TEAMS_VIEW */,
1419
+ "user_teams_nominate_peer" /* USER_TEAMS_NOMINATE_PEER */,
1420
+ "user_cycles_view" /* USER_CYCLES_VIEW */,
1421
+ "user_cycles_self_review_generate" /* USER_CYCLES_SELF_REVIEW_GENERATE */,
1422
+ "user_cycles_self_review_submit" /* USER_CYCLES_SELF_REVIEW_SUBMIT */
1423
+ ];
1424
+ var ENABLED_UNIVERSAL = Object.fromEntries(
1425
+ UNIVERSAL_FEATURES.map((f) => [f, { enabled: true }])
1426
+ );
1427
+ var DISABLED_PLUS_GATED = Object.fromEntries(
1428
+ PLUS_GATED_FEATURES.map((f) => [f, { enabled: false }])
1429
+ );
1430
+ var ENABLED_PLUS_GATED = Object.fromEntries(
1431
+ PLUS_GATED_FEATURES.map((f) => [f, { enabled: true }])
1432
+ );
1433
+ var DISABLED_PRO_GATED = Object.fromEntries(
1434
+ PRO_GATED_FEATURES.map((f) => [f, { enabled: false }])
1435
+ );
1436
+ var ENABLED_PRO_GATED = Object.fromEntries(
1437
+ PRO_GATED_FEATURES.map((f) => [f, { enabled: true }])
1438
+ );
1439
+ var DISABLED_TEAM_GATED = Object.fromEntries(
1440
+ TEAM_GATED_FEATURES.map((f) => [f, { enabled: false }])
1441
+ );
1442
+ var PRO_TIER_TOKEN_BUDGET = 1e6;
1443
+ var TIER_FEATURES = {
1444
+ FREE: {
1445
+ tier: "FREE",
1446
+ features: {
1447
+ ...DISABLED_ORG_TEAM_FEATURES,
1448
+ ...ENABLED_UNIVERSAL,
1449
+ ...DISABLED_PLUS_GATED,
1450
+ ...DISABLED_PRO_GATED,
1451
+ ...DISABLED_TEAM_GATED,
1452
+ ["page_team_view" /* PAGE_TEAM_VIEW */]: { enabled: false },
1453
+ ["basic_brags" /* BASIC_BRAGS */]: { enabled: true },
1454
+ ["basic_organizations" /* BASIC_ORGANIZATIONS */]: { enabled: true, quota: 2 },
1455
+ ["ai_report_generation" /* AI_REPORT_GENERATION */]: {
1456
+ enabled: true,
1457
+ quota: 2,
1458
+ resetPeriod: "monthly"
1459
+ },
1460
+ ["pdf_export" /* PDF_EXPORT */]: { enabled: true, quota: 3, resetPeriod: "monthly" },
1461
+ ["share_links" /* SHARE_LINKS */]: { enabled: true, quota: 5 },
1462
+ // max 5 active
1463
+ ["objectives" /* OBJECTIVES */]: { enabled: true, quota: 3 },
1464
+ // max 3 active
1465
+ ["advanced_notifications" /* ADVANCED_NOTIFICATIONS */]: { enabled: false },
1466
+ ["full_export_import" /* FULL_EXPORT_IMPORT */]: { enabled: false },
1467
+ ["cli_turbo_mode" /* CLI_TURBO_MODE */]: { enabled: false },
1468
+ ["priority_support" /* PRIORITY_SUPPORT */]: { enabled: false },
1469
+ ["early_access" /* EARLY_ACCESS */]: { enabled: false },
1470
+ ["smart_goal_suggestions" /* SMART_GOAL_SUGGESTIONS */]: { enabled: false },
1471
+ ["ai_powered_suggestions" /* AI_POWERED_SUGGESTIONS */]: { enabled: false },
1472
+ ["ai_token_budget" /* AI_TOKEN_BUDGET */]: { enabled: false },
1473
+ ["generate_objectives" /* GENERATE_OBJECTIVES */]: { enabled: false },
1474
+ ["multi_email" /* MULTI_EMAIL */]: { enabled: false }
1475
+ }
1476
+ },
1477
+ PLUS: {
1478
+ tier: "PLUS",
1479
+ features: {
1480
+ ...DISABLED_ORG_TEAM_FEATURES,
1481
+ ...ENABLED_UNIVERSAL,
1482
+ ...ENABLED_PLUS_GATED,
1483
+ ...DISABLED_PRO_GATED,
1484
+ ...DISABLED_TEAM_GATED,
1485
+ ["page_team_view" /* PAGE_TEAM_VIEW */]: { enabled: false },
1486
+ ["basic_brags" /* BASIC_BRAGS */]: { enabled: true },
1487
+ ["basic_organizations" /* BASIC_ORGANIZATIONS */]: { enabled: true },
1488
+ ["ai_report_generation" /* AI_REPORT_GENERATION */]: { enabled: true, quota: 30, resetPeriod: "monthly" },
1489
+ ["pdf_export" /* PDF_EXPORT */]: {
1490
+ enabled: true,
1491
+ quota: 15,
1492
+ resetPeriod: "monthly"
1493
+ },
1494
+ ["share_links" /* SHARE_LINKS */]: { enabled: true, quota: 20 },
1495
+ // max 20 active
1496
+ ["objectives" /* OBJECTIVES */]: { enabled: true, quota: 4 },
1497
+ // max 4 active
1498
+ ["advanced_notifications" /* ADVANCED_NOTIFICATIONS */]: { enabled: true },
1499
+ ["full_export_import" /* FULL_EXPORT_IMPORT */]: { enabled: true },
1500
+ ["cli_turbo_mode" /* CLI_TURBO_MODE */]: { enabled: false },
1501
+ ["priority_support" /* PRIORITY_SUPPORT */]: { enabled: true },
1502
+ ["early_access" /* EARLY_ACCESS */]: { enabled: false },
1503
+ ["smart_goal_suggestions" /* SMART_GOAL_SUGGESTIONS */]: { enabled: true },
1504
+ ["ai_powered_suggestions" /* AI_POWERED_SUGGESTIONS */]: { enabled: true, quota: 30, resetPeriod: "monthly" },
1505
+ ["ai_token_budget" /* AI_TOKEN_BUDGET */]: { enabled: false },
1506
+ ["generate_objectives" /* GENERATE_OBJECTIVES */]: { enabled: true },
1507
+ ["multi_email" /* MULTI_EMAIL */]: { enabled: false }
1508
+ }
1509
+ },
1510
+ PRO: {
1511
+ tier: "PRO",
1512
+ features: {
1513
+ ...DISABLED_ORG_TEAM_FEATURES,
1514
+ ...ENABLED_UNIVERSAL,
1515
+ ...ENABLED_PLUS_GATED,
1516
+ ...ENABLED_PRO_GATED,
1517
+ ...DISABLED_TEAM_GATED,
1518
+ ["page_team_view" /* PAGE_TEAM_VIEW */]: { enabled: false },
1519
+ ["basic_brags" /* BASIC_BRAGS */]: { enabled: true },
1520
+ ["basic_organizations" /* BASIC_ORGANIZATIONS */]: { enabled: true },
1521
+ ["ai_report_generation" /* AI_REPORT_GENERATION */]: {
1522
+ enabled: true,
1523
+ tokenQuota: PRO_TIER_TOKEN_BUDGET,
1524
+ resetPeriod: "monthly"
1525
+ },
1526
+ ["pdf_export" /* PDF_EXPORT */]: { enabled: true },
1527
+ // unlimited
1528
+ ["share_links" /* SHARE_LINKS */]: { enabled: true },
1529
+ // unlimited
1530
+ ["objectives" /* OBJECTIVES */]: { enabled: true },
1531
+ // unlimited
1532
+ ["advanced_notifications" /* ADVANCED_NOTIFICATIONS */]: { enabled: true },
1533
+ ["full_export_import" /* FULL_EXPORT_IMPORT */]: { enabled: true },
1534
+ ["cli_turbo_mode" /* CLI_TURBO_MODE */]: { enabled: true },
1535
+ ["priority_support" /* PRIORITY_SUPPORT */]: { enabled: true },
1536
+ ["early_access" /* EARLY_ACCESS */]: { enabled: true },
1537
+ ["smart_goal_suggestions" /* SMART_GOAL_SUGGESTIONS */]: { enabled: true },
1538
+ ["ai_powered_suggestions" /* AI_POWERED_SUGGESTIONS */]: { enabled: true },
1539
+ ["ai_token_budget" /* AI_TOKEN_BUDGET */]: {
1540
+ enabled: true,
1541
+ tokenQuota: PRO_TIER_TOKEN_BUDGET,
1542
+ resetPeriod: "monthly"
1543
+ },
1544
+ ["generate_objectives" /* GENERATE_OBJECTIVES */]: { enabled: true },
1545
+ ["multi_email" /* MULTI_EMAIL */]: { enabled: true }
1546
+ }
1547
+ }
1548
+ };
1549
+
1550
+ // ../server/src/utils/access-control/roles.ts
1551
+ function deriveEnabledFeatures(tier) {
1552
+ const config = TIER_FEATURES[tier];
1553
+ if (!config) return [];
1554
+ return Object.entries(config.features).filter(([, limit]) => limit.enabled).map(([f]) => f);
1555
+ }
1556
+ var PLAN_ROLE_FEATURES = {
1557
+ "plan:FREE": deriveEnabledFeatures("FREE"),
1558
+ "plan:PLUS": deriveEnabledFeatures("PLUS"),
1559
+ "plan:PRO": deriveEnabledFeatures("PRO")
1560
+ };
1561
+ var ORG_ADMIN_SHARED_FEATURES = [
1562
+ "org_access" /* ORG_ACCESS */,
1563
+ "org_mutate_base" /* ORG_MUTATE_BASE */,
1564
+ "org_admin_access" /* ORG_ADMIN_ACCESS */,
1565
+ "org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */,
1566
+ "manager_tier_access" /* MANAGER_TIER_ACCESS */,
1567
+ "org_members_manage" /* ORG_MEMBERS_MANAGE */,
1568
+ "org_invites_manage" /* ORG_INVITES_MANAGE */,
1569
+ "org_teams_manage" /* ORG_TEAMS_MANAGE */,
1570
+ "org_growth_view" /* ORG_GROWTH_VIEW */,
1571
+ "org_career_metrics_manage" /* ORG_CAREER_METRICS_MANAGE */,
1572
+ "org_cycles_manage" /* ORG_CYCLES_MANAGE */,
1573
+ "org_stats_view" /* ORG_STATS_VIEW */,
1574
+ "org_audit_log_view" /* ORG_AUDIT_LOG_VIEW */,
1575
+ "org_billing_manage" /* ORG_BILLING_MANAGE */,
1576
+ "org_notifications_manage" /* ORG_NOTIFICATIONS_MANAGE */
1577
+ ];
1578
+ var ORG_ROLE_FEATURES = {
1579
+ "org:org_owner": [...ORG_ADMIN_SHARED_FEATURES, "org_owner_access" /* ORG_OWNER_ACCESS */, "page_team_view" /* PAGE_TEAM_VIEW */, ...TEAM_GATED_FEATURES],
1580
+ "org:org_admin": [...ORG_ADMIN_SHARED_FEATURES, "page_team_view" /* PAGE_TEAM_VIEW */, ...TEAM_GATED_FEATURES],
1581
+ // growth_admin: matches canManageGrowth (owner/admin/growth_admin) but NOT
1582
+ // isOrgAdminRole (owner/admin only) — no ORG_ADMIN_ACCESS or its dependents
1583
+ // (members/invites/teams/stats/audit-log/billing/notifications manage).
1584
+ "org:growth_admin": [
1585
+ "org_access" /* ORG_ACCESS */,
1586
+ "org_mutate_base" /* ORG_MUTATE_BASE */,
1587
+ "org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */,
1588
+ "manager_tier_access" /* MANAGER_TIER_ACCESS */,
1589
+ "org_growth_view" /* ORG_GROWTH_VIEW */,
1590
+ "org_career_metrics_manage" /* ORG_CAREER_METRICS_MANAGE */,
1591
+ "org_cycles_manage" /* ORG_CYCLES_MANAGE */,
1592
+ "page_team_view" /* PAGE_TEAM_VIEW */,
1593
+ ...TEAM_GATED_FEATURES
1594
+ ],
1595
+ "org:org_member": ["org_access" /* ORG_ACCESS */, "org_mutate_base" /* ORG_MUTATE_BASE */, "page_team_view" /* PAGE_TEAM_VIEW */, ...TEAM_GATED_FEATURES],
1596
+ // org_viewer: read-only — gets view/read subset of team features only.
1597
+ "org:org_viewer": [
1598
+ "org_access" /* ORG_ACCESS */,
1599
+ "page_team_view" /* PAGE_TEAM_VIEW */,
1600
+ "evaluation_comments_read" /* EVALUATION_COMMENTS_READ */,
1601
+ "peer_reviews_view" /* PEER_REVIEWS_VIEW */,
1602
+ "user_teams_view" /* USER_TEAMS_VIEW */,
1603
+ "user_cycles_view" /* USER_CYCLES_VIEW */
1604
+ ]
1605
+ };
1606
+ var TEAM_ROLE_FEATURES = {
1607
+ "team:owner": [
1608
+ "team_manage" /* TEAM_MANAGE */,
1609
+ "team_owner_actions" /* TEAM_OWNER_ACTIONS */,
1610
+ "team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
1611
+ "manager_tier_access" /* MANAGER_TIER_ACCESS */,
1612
+ "page_team_view" /* PAGE_TEAM_VIEW */,
1613
+ ...TEAM_GATED_FEATURES
1614
+ ],
1615
+ "team:co_manager": [
1616
+ "team_manage" /* TEAM_MANAGE */,
1617
+ "team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
1618
+ "manager_tier_access" /* MANAGER_TIER_ACCESS */,
1619
+ "page_team_view" /* PAGE_TEAM_VIEW */,
1620
+ ...TEAM_GATED_FEATURES
1621
+ ],
1622
+ "team:member": [
1623
+ "team_membership_view" /* TEAM_MEMBERSHIP_VIEW */,
1624
+ "page_team_view" /* PAGE_TEAM_VIEW */,
1625
+ ...TEAM_GATED_FEATURES
1626
+ ]
1627
+ };
1628
+ var ROLE_FEATURES = {
1629
+ ...PLAN_ROLE_FEATURES,
1630
+ ...ORG_ROLE_FEATURES,
1631
+ ...TEAM_ROLE_FEATURES
1632
+ };
1633
+ var ORG_GROWTH_CAPABLE_ROLE_IDS = Object.keys(ORG_ROLE_FEATURES).filter((id) => ROLE_FEATURES[id].includes("org_growth_admin_access" /* ORG_GROWTH_ADMIN_ACCESS */));
1634
+ var ORG_GROWTH_CAPABLE_ROLES = ORG_GROWTH_CAPABLE_ROLE_IDS.map(
1635
+ (id) => id.replace("org:", "")
1636
+ );
1637
+
1638
+ // ../server/src/utils/badges/badge.constants.ts
1639
+ var BadgeKeys = {
1640
+ // Milestone - Brags
1641
+ FIRST_BRAG: "first_brag",
1642
+ BRAGS_10: "10_brags",
1643
+ BRAGS_25: "25_brags",
1644
+ BRAGS_100: "100_brags",
1645
+ BRAGS_500: "500_brags",
1646
+ // Milestone - Features
1647
+ FIRST_OBJECTIVE: "first_objective",
1648
+ FIRST_COMPANY: "first_company",
1649
+ FIRST_REPORT: "first_report",
1650
+ // Streaks
1651
+ STREAK_7_DAY: "7_day_streak",
1652
+ STREAK_30_DAY: "30_day_streak",
1653
+ STREAK_90_DAY: "90_day_streak",
1654
+ // Goal Completion
1655
+ FIRST_OBJECTIVE_COMPLETED: "first_objective_completed",
1656
+ OBJECTIVES_COMPLETED_5: "5_objectives_completed",
1657
+ OBJECTIVES_COMPLETED_25: "25_objectives_completed"
1658
+ };
1659
+ var BadgeCategories = {
1660
+ MILESTONE: "milestone",
1661
+ STREAK: "streak",
1662
+ GOAL: "goal"
1663
+ };
1664
+ var BADGE_DEFINITIONS = [
1665
+ // Milestone - Brags
1666
+ { key: BadgeKeys.FIRST_BRAG, category: BadgeCategories.MILESTONE, threshold: 1 },
1667
+ { key: BadgeKeys.BRAGS_10, category: BadgeCategories.MILESTONE, threshold: 10 },
1668
+ { key: BadgeKeys.BRAGS_25, category: BadgeCategories.MILESTONE, threshold: 25 },
1669
+ { key: BadgeKeys.BRAGS_100, category: BadgeCategories.MILESTONE, threshold: 100 },
1670
+ { key: BadgeKeys.BRAGS_500, category: BadgeCategories.MILESTONE, threshold: 500 },
1671
+ // Milestone - Features
1672
+ { key: BadgeKeys.FIRST_OBJECTIVE, category: BadgeCategories.MILESTONE, threshold: 1 },
1673
+ { key: BadgeKeys.FIRST_COMPANY, category: BadgeCategories.MILESTONE, threshold: 1 },
1674
+ { key: BadgeKeys.FIRST_REPORT, category: BadgeCategories.MILESTONE, threshold: 1 },
1675
+ // Streaks
1676
+ { key: BadgeKeys.STREAK_7_DAY, category: BadgeCategories.STREAK, threshold: 7 },
1677
+ { key: BadgeKeys.STREAK_30_DAY, category: BadgeCategories.STREAK, threshold: 30 },
1678
+ { key: BadgeKeys.STREAK_90_DAY, category: BadgeCategories.STREAK, threshold: 90 },
1679
+ // Goal Completion
1680
+ {
1681
+ key: BadgeKeys.FIRST_OBJECTIVE_COMPLETED,
1682
+ category: BadgeCategories.GOAL,
1683
+ threshold: 1
1684
+ },
1685
+ { key: BadgeKeys.OBJECTIVES_COMPLETED_5, category: BadgeCategories.GOAL, threshold: 5 },
1686
+ { key: BadgeKeys.OBJECTIVES_COMPLETED_25, category: BadgeCategories.GOAL, threshold: 25 }
1687
+ ];
1688
+ var BRAG_MILESTONE_BADGES = [
1689
+ BadgeKeys.FIRST_BRAG,
1690
+ BadgeKeys.BRAGS_10,
1691
+ BadgeKeys.BRAGS_25,
1692
+ BadgeKeys.BRAGS_100,
1693
+ BadgeKeys.BRAGS_500
1694
+ ];
1695
+ var STREAK_BADGES = [
1696
+ BadgeKeys.STREAK_7_DAY,
1697
+ BadgeKeys.STREAK_30_DAY,
1698
+ BadgeKeys.STREAK_90_DAY
1699
+ ];
1700
+ var OBJECTIVE_COMPLETED_BADGES = [
1701
+ BadgeKeys.FIRST_OBJECTIVE_COMPLETED,
1702
+ BadgeKeys.OBJECTIVES_COMPLETED_5,
1703
+ BadgeKeys.OBJECTIVES_COMPLETED_25
1704
+ ];
1705
+
1706
+ // ../../packages/types/domain/story-categories.ts
1707
+ var STORY_CATEGORY_LABELS = {
1708
+ leadership: "Leadership",
1709
+ technical_excellence: "Technical Excellence",
1710
+ problem_solving: "Problem Solving",
1711
+ innovation: "Innovation",
1712
+ teamwork: "Teamwork",
1713
+ communication: "Communication",
1714
+ project_delivery: "Project Delivery",
1715
+ customer_impact: "Customer Impact",
1716
+ mentorship: "Mentorship",
1717
+ process_improvement: "Process Improvement"
1718
+ };
1719
+
1720
+ // src/tools/list-stories.ts
1721
+ var listStoriesTool = {
1722
+ name: "list_stories",
1723
+ description: "List STAR interview stories from BragDuck. Use when the user wants to review their interview stories or search for stories by category.",
1724
+ inputSchema: {
1725
+ type: "object",
1726
+ properties: {
1727
+ limit: {
1728
+ type: "number",
1729
+ minimum: 1,
1730
+ maximum: 100,
1731
+ description: "Max stories to return (default 10)"
1732
+ },
1733
+ category: {
1734
+ type: "string",
1735
+ description: "Filter by category (e.g. leadership, technical_excellence, problem_solving)"
1736
+ },
1737
+ orgId: {
1738
+ type: "string",
1739
+ description: "Filter by organisation ID"
1740
+ },
1741
+ search: {
1742
+ type: "string",
1743
+ description: "Search stories by keyword"
1744
+ }
1745
+ }
1746
+ }
1747
+ };
1748
+ async function handleListStories(input) {
1749
+ const planError = await requirePaidPlan();
1750
+ if (planError) return planError;
1751
+ try {
1752
+ const userInfo = getUserInfo();
1753
+ if (!userInfo) {
1754
+ return {
1755
+ content: [{
1756
+ type: "text",
1757
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1758
+ }],
1759
+ isError: true
1760
+ };
1761
+ }
1762
+ const params = {
1763
+ page: "1",
1764
+ perPage: String(input.limit || 10)
1765
+ };
1766
+ if (input.category) params.category = input.category;
1767
+ if (input.orgId) params.orgId = input.orgId;
1768
+ if (input.search) params.search = input.search;
1769
+ const response = await apiRequest(
1770
+ `/v1/users/${userInfo.id}/stories`,
1771
+ { params }
1772
+ );
1773
+ if (response.items.length === 0) {
1774
+ return {
1775
+ content: [{ type: "text", text: "No stories found." }]
1776
+ };
1777
+ }
1778
+ const lines = response.items.map((story) => {
1779
+ const categoryLabel = story.category ? STORY_CATEGORY_LABELS[story.category] : null;
1780
+ const categoryPart = categoryLabel ? `Category: ${categoryLabel} | ` : "";
1781
+ const strengthPart = `Strength: ${story.strengthScore ?? "-"}/10`;
1782
+ const impactPart = `Impact: ${story.impactScore ?? "-"}/10`;
1783
+ const summary2 = story.aiSummary || "No AI summary yet";
1784
+ const competencies = story.competencies.length > 0 ? story.competencies.join(", ") : "None";
1785
+ return `**${story.title}** (ID: ${story.id})
1786
+ ${categoryPart}${strengthPart} | ${impactPart}
1787
+ ${summary2}
1788
+ Competencies: ${competencies}`;
1789
+ });
1790
+ const summary = `Found ${response.total} stories (showing ${response.items.length}):`;
1791
+ return {
1792
+ content: [{ type: "text", text: `${summary}
1793
+
1794
+ ${lines.join("\n\n")}` }]
1795
+ };
1796
+ } catch (error) {
1797
+ if (error instanceof Error && (error.message === "NOT_AUTHENTICATED" || error.message === "TOKEN_EXPIRED")) {
1798
+ return {
1799
+ content: [{
1800
+ type: "text",
1801
+ text: 'Not authenticated with BragDuck. Please call the "login" tool first.'
1802
+ }],
1803
+ isError: true
1804
+ };
1805
+ }
1806
+ return {
1807
+ content: [{
1808
+ type: "text",
1809
+ text: `Failed to list stories: ${error instanceof Error ? error.message : String(error)}`
1810
+ }],
1811
+ isError: true
1812
+ };
1813
+ }
1814
+ }
1815
+
1816
+ // src/index.ts
1817
+ var server = new Server(
1818
+ {
1819
+ name: "bragduck",
1820
+ version: "1.0.0"
1821
+ },
1822
+ {
1823
+ capabilities: {
1824
+ tools: {}
1825
+ },
1826
+ instructions: 'BragDuck tracks career achievements ("brags") that feed into periodic performance reports. A strong brag needs: a concrete accomplishment, measurable or observable impact, and ideally a reference link. \n\nRESOLVING "THIS" IN "BRAG WITH THIS"\nWhen the user says "brag with this" (or "brag about this", "log this", etc.) without elaborating, "this" refers to the most recent work visible in the current session: the latest git diff, the last commit message, the most recently discussed code change, or a PR just mentioned. Look for that context first before asking anything. Only start the interview (see below) if no such context exists in the session.\n\nSTEP 1 \u2014 ASSESS BRAGABILITY\nBefore doing anything else, judge whether the subject is worth logging:\n- NOT bragable: vague status updates, trivial tasks with no impact, or things a reasonable person would not list on a performance review. Tell the user directly that it is not bragable and explain why in one sentence. Note that brags feed the AI report generator \u2014 weak entries dilute the signal.\n- POTENTIALLY bragable but thin: start the interview (see below).\n- Clearly bragable with full context: skip the interview, go straight to STEP 2.\n\nTHE INTERVIEW\nConduct a short conversational interview to gather what you need. Ask ONE question at a time \u2014 do not list multiple questions at once. Each question should follow naturally from the previous answer, like a real conversation. Suggested question order (adapt based on what you already know):\n 1. "What did you work on?" (skip if already known)\n 2. "What was the outcome or result?"\n 3. "Who was affected \u2014 just you, the team, or wider?"\n 4. "Do you have a link \u2014 PR, ticket, commit, or doc?" (skip if already have one)\nStop the interview as soon as you have enough to write a strong brag. Never ask more than 4 questions total.\n\nSTEP 2 \u2014 SELECT ORGANISATION\nBefore composing the brag, determine which company it belongs to:\n1. Call list_organisations. It returns the org list AND any saved preference for the current directory.\n2. If a saved preference exists, use that orgId silently \u2014 do not ask the user.\n3. If no saved preference, present the org list to the user and ask: "Which company does this belong to?" Wait for their answer, then call save_org_preference with the chosen orgId and orgName.\n4. If the user has only one organisation, select it automatically and save the preference.\n5. If the user has no organisations, skip this step and proceed without an orgId.\n\nSTEP 3 \u2014 PREVIEW + LOG\nCompose the brag from all gathered context. Do NOT call preview_brag. Instead, in your response text render the brag fields as a markdown table with two columns (Field | Value), one row per field, in this order: Title, Type, Impact Level, Impact Details, Description, Tags, References, Organisation. In the very same response, after the table, call log_brag with those field values (including orgId). The native tool-permission prompt (Allow / Deny) that appears below the table IS the confirmation \u2014 do not add any text question. If the user denies or asks for changes, adjust and repeat this step.\n\nSTEP 4 \u2014 DONE\nOnce log_brag is allowed and succeeds, confirm with one short sentence.\n\nQUICK BRAG (via /brag or raw text)\nWhen the user says "/brag" followed by text, or pastes a long block of text and asks to log it, use the quick_brag tool instead of the interview flow. quick_brag accepts raw text and uses AI to extract and structure it into a proper brag automatically. Skip the interview, skip the preview table \u2014 just call quick_brag with the text.\n\nINTERVIEW STORIES (log_story)\nWhen the user discusses interview prep, behavioral questions, or asks to save a STAR story, use log_story instead of log_brag. Stories are structured with Situation/Task/Action/Result fields and competency tags. Follow the same org-selection flow (list_organisations \u2192 saved preference or ask). Preview the story as a markdown table (Field | Value) before calling log_story in the same response.\nUse list_stories to browse existing stories. Supports filters: category, orgId, search, limit. Stories include AI-generated strength and impact scores.\n\nGENERAL RULES\n- Never fabricate details, metrics, or links. If unsure, ask.\n- "Is this bragable?" deserves a direct honest answer \u2014 not a tool call.\n- Call login first if any tool returns an authentication error.'
1827
+ }
1828
+ );
1829
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1830
+ return {
1831
+ tools: [loginTool, logBragTool, quickBragTool, listBragsTool, listObjectivesTool, linkBragToKeyResultTool, listOrganisationsTool, saveOrgPreferenceTool, logStoryTool, listStoriesTool]
1832
+ };
96
1833
  });
97
1834
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
98
- const { name, arguments: args } = request.params;
99
- switch (name) {
100
- case 'login':
101
- return handleLogin(args);
102
- case 'preview_brag':
103
- return handlePreviewBrag(args);
104
- case 'log_brag':
105
- return handleLogBrag(args);
106
- case 'quick_brag':
107
- return handleQuickBrag(args);
108
- case 'list_brags':
109
- return handleListBrags(args);
110
- case 'list_objectives':
111
- return handleListObjectives();
112
- case 'link_brag_to_key_result':
113
- return handleLinkBragToKeyResult(args);
114
- case 'list_organisations':
115
- return handleListOrganisations();
116
- case 'save_org_preference':
117
- return handleSaveOrgPreference(args);
118
- case 'log_story':
119
- return handleLogStory(args);
120
- default:
121
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
122
- }
1835
+ const { name, arguments: args } = request.params;
1836
+ switch (name) {
1837
+ case "login":
1838
+ return handleLogin(args);
1839
+ case "preview_brag":
1840
+ return handlePreviewBrag(args);
1841
+ case "log_brag":
1842
+ return handleLogBrag(args);
1843
+ case "quick_brag":
1844
+ return handleQuickBrag(args);
1845
+ case "list_brags":
1846
+ return handleListBrags(args);
1847
+ case "list_objectives":
1848
+ return handleListObjectives();
1849
+ case "link_brag_to_key_result":
1850
+ return handleLinkBragToKeyResult(args);
1851
+ case "list_organisations":
1852
+ return handleListOrganisations();
1853
+ case "save_org_preference":
1854
+ return handleSaveOrgPreference(args);
1855
+ case "log_story":
1856
+ return handleLogStory(args);
1857
+ case "list_stories":
1858
+ return handleListStories(args);
1859
+ default:
1860
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1861
+ }
123
1862
  });
124
- const transport = new StdioServerTransport();
1863
+ var transport = new StdioServerTransport();
125
1864
  await server.connect(transport);
126
1865
  //# sourceMappingURL=index.js.map