@hailer/mcp 1.0.28 → 1.0.30

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/cli.js CHANGED
@@ -47,6 +47,9 @@ const os = __importStar(require("os"));
47
47
  const readline = __importStar(require("readline"));
48
48
  const CLAUDE_CONFIG_DIR = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
49
49
  const CLAUDE_CONFIG_FILE = path.join(CLAUDE_CONFIG_DIR, 'claude_desktop_config.json');
50
+ // Credentials file for publish and other tools that need direct auth
51
+ const HAILER_MCP_DIR = path.join(os.homedir(), '.hailer-mcp');
52
+ const CREDENTIALS_FILE = path.join(HAILER_MCP_DIR, 'credentials.json');
50
53
  /**
51
54
  * Muted output stream for password input
52
55
  */
@@ -55,6 +58,10 @@ class MutedStream {
55
58
  muted = false;
56
59
  write() { return true; }
57
60
  end() { }
61
+ on() { return this; }
62
+ once() { return this; }
63
+ emit() { return true; }
64
+ removeListener() { return this; }
58
65
  }
59
66
  /**
60
67
  * Prompt user for input
@@ -199,8 +206,19 @@ async function runSetup() {
199
206
  }
200
207
  // Write config
201
208
  fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(config, null, 2));
209
+ // Save credentials to ~/.hailer-mcp/credentials.json for publish tool
210
+ if (!fs.existsSync(HAILER_MCP_DIR)) {
211
+ fs.mkdirSync(HAILER_MCP_DIR, { recursive: true });
212
+ }
213
+ const credentials = {
214
+ email,
215
+ password,
216
+ apiUrl,
217
+ };
218
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
202
219
  console.log('\nāœ… Configuration saved!');
203
- console.log(` File: ${CLAUDE_CONFIG_FILE}`);
220
+ console.log(` Claude Desktop: ${CLAUDE_CONFIG_FILE}`);
221
+ console.log(` Credentials: ${CREDENTIALS_FILE}`);
204
222
  console.log('\nšŸš€ Next steps:');
205
223
  console.log(' 1. Quit Claude Desktop completely (Cmd+Q)');
206
224
  console.log(' 2. Reopen Claude Desktop');
@@ -1,6 +1,8 @@
1
1
  import { HailerClient } from './hailer-clients';
2
2
  import { WorkspaceCache } from './workspace-cache';
3
3
  import { HailerV2CoreInitResponse, HailerApiClient } from './utils/index';
4
+ import { ToolGroup } from './tool-registry';
5
+ import { UserRole } from './utils/index';
4
6
  export interface UserContext {
5
7
  client: HailerClient;
6
8
  hailer: HailerApiClient;
@@ -10,6 +12,9 @@ export interface UserContext {
10
12
  createdAt: number;
11
13
  email: string;
12
14
  password: string;
15
+ workspaceRoles: Record<string, UserRole>;
16
+ currentWorkspaceId: string;
17
+ allowedGroups: ToolGroup[];
13
18
  }
14
19
  /**
15
20
  * Cache for user-specific data (client connections, init data, workspace cache)
@@ -6,6 +6,7 @@ const workspace_cache_1 = require("./workspace-cache");
6
6
  const config_1 = require("../config");
7
7
  const logger_1 = require("../lib/logger");
8
8
  const index_1 = require("./utils/index");
9
+ const index_2 = require("./utils/index");
9
10
  const logger = (0, logger_1.createLogger)({ component: 'user-context-cache' });
10
11
  /**
11
12
  * Cache for user-specific data (client connections, init data, workspace cache)
@@ -123,6 +124,21 @@ class UserContextCache {
123
124
  // Create workspace cache from init data
124
125
  const appConfig = (0, config_1.createApplicationConfig)();
125
126
  const workspaceCache = (0, workspace_cache_1.createWorkspaceCache)(init, appConfig.mcpConfig);
127
+ // Extract user roles from ALL workspaces
128
+ const currentUserId = await (0, hailer_clients_1.getCurrentUserId)(client);
129
+ const workspaceRoles = (0, index_2.extractWorkspaceRoles)(networks, currentUserId);
130
+ // Get current workspace ID
131
+ const currentWorkspaceId = init.network?._id || Object.keys(networks)[0] || '';
132
+ // Get role for current workspace (for backward compatibility and initial tool filtering)
133
+ const userRole = workspaceRoles[currentWorkspaceId] || 'guest';
134
+ const allowedGroups = (0, index_2.getAllowedGroups)(userRole, config_1.environment.ENABLE_NUCLEAR_TOOLS);
135
+ logger.info('User roles extracted from all workspaces', {
136
+ apiKey: apiKey.substring(0, 8) + '...',
137
+ workspaceCount: Object.keys(workspaceRoles).length,
138
+ currentWorkspaceId,
139
+ currentRole: userRole,
140
+ allRoles: Object.entries(workspaceRoles).map(([id, role]) => `${id.slice(-6)}:${role}`).join(', ')
141
+ });
126
142
  // Get credentials from config (for tools like publish_hailer_app that need external auth)
127
143
  const accountConfig = appConfig.getClientConfig(apiKey);
128
144
  const context = {
@@ -134,6 +150,9 @@ class UserContextCache {
134
150
  createdAt: Date.now(),
135
151
  email: accountConfig.email,
136
152
  password: accountConfig.password,
153
+ workspaceRoles, // NEW: Map of workspaceId → role
154
+ currentWorkspaceId, // NEW: Current workspace ID
155
+ allowedGroups, // Keep for stdio-server compatibility
137
156
  };
138
157
  // Calculate and log cache sizes
139
158
  const rawInitSize = Buffer.byteLength(JSON.stringify(init), 'utf8');
@@ -27,6 +27,20 @@ export declare enum ToolGroup {
27
27
  NUCLEAR = "nuclear",
28
28
  BOT_INTERNAL = "bot_internal"
29
29
  }
30
+ /**
31
+ * Tool annotations for MCP safety hints (per MCP spec)
32
+ * These hints help clients understand tool behavior for UI/UX decisions
33
+ */
34
+ export interface ToolAnnotations {
35
+ /** If true, the tool does not modify any data (safe to run without confirmation) */
36
+ readOnlyHint?: boolean;
37
+ /** If true, the tool may perform destructive updates (delete, overwrite) */
38
+ destructiveHint?: boolean;
39
+ /** If true, the tool may interact with entities outside the user's control */
40
+ openWorldHint?: boolean;
41
+ /** If true, repeated calls with same args produce same result */
42
+ idempotentHint?: boolean;
43
+ }
30
44
  /**
31
45
  * Tool definition interface
32
46
  */
@@ -35,6 +49,7 @@ export interface Tool<TSchema extends z.ZodType = z.ZodType> {
35
49
  group: ToolGroup;
36
50
  description: string;
37
51
  schema: TSchema;
52
+ annotations?: ToolAnnotations;
38
53
  execute: (args: z.infer<TSchema>, context: UserContext) => Promise<McpResponse>;
39
54
  }
40
55
  /**
@@ -44,6 +59,7 @@ export interface ToolDefinition {
44
59
  name: string;
45
60
  description: string;
46
61
  inputSchema: any;
62
+ annotations?: ToolAnnotations;
47
63
  }
48
64
  /**
49
65
  * ToolRegistry - Clean, testable, dependency-injected tool registry
@@ -192,7 +192,8 @@ class ToolRegistry {
192
192
  return toolsToExpose.map(tool => ({
193
193
  name: tool.name,
194
194
  description: tool.description,
195
- inputSchema: this.convertZodSchemaToJsonSchema(tool.schema)
195
+ inputSchema: this.convertZodSchemaToJsonSchema(tool.schema),
196
+ ...(tool.annotations && { annotations: tool.annotations })
196
197
  }));
197
198
  }
198
199
  /**
@@ -222,7 +223,8 @@ class ToolRegistry {
222
223
  return {
223
224
  name: tool.name,
224
225
  description: `${tool.description} [Enum-constrained to valid workspace IDs]`,
225
- inputSchema: masterSchema
226
+ inputSchema: masterSchema,
227
+ ...(tool.annotations && { annotations: tool.annotations })
226
228
  };
227
229
  }
228
230
  }
@@ -234,7 +236,8 @@ class ToolRegistry {
234
236
  return {
235
237
  name: tool.name,
236
238
  description: `${tool.description} [Schema-constrained to ${workflowName}]`,
237
- inputSchema: workspaceSchema
239
+ inputSchema: workspaceSchema,
240
+ ...(tool.annotations && { annotations: tool.annotations })
238
241
  };
239
242
  }
240
243
  }
@@ -242,7 +245,8 @@ class ToolRegistry {
242
245
  return {
243
246
  name: tool.name,
244
247
  description: tool.description,
245
- inputSchema: this.convertZodSchemaToJsonSchema(tool.schema)
248
+ inputSchema: this.convertZodSchemaToJsonSchema(tool.schema),
249
+ ...(tool.annotations && { annotations: tool.annotations })
246
250
  };
247
251
  }
248
252
  /**
@@ -436,6 +436,7 @@ exports.listActivitiesTool = {
436
436
  name: 'list_activities',
437
437
  group: tool_registry_1.ToolGroup.READ,
438
438
  description: `List activities from workflow phase`,
439
+ annotations: { readOnlyHint: true },
439
440
  schema: zod_1.z.object({
440
441
  workspaceId: zod_1.z.string().optional().describe("Workspace ID. If not provided, uses current workspace. Use list_my_workspaces to see available workspaces."),
441
442
  workflowId: zod_1.z.string().describe("Workflow ID to list activities from"),
@@ -584,6 +585,7 @@ exports.showActivityByIdTool = {
584
585
  name: 'show_activity_by_id',
585
586
  group: tool_registry_1.ToolGroup.READ,
586
587
  description: `Get activity by ID`,
588
+ annotations: { readOnlyHint: true },
587
589
  schema: zod_1.z.object({
588
590
  workspaceId: zod_1.z.string().optional().describe("Workspace ID. If not provided, uses current workspace. Use list_my_workspaces to see available workspaces."),
589
591
  activityId: zod_1.z.string().describe("Activity ID to load"),
@@ -631,6 +633,7 @@ exports.createActivityTool = {
631
633
  name: 'create_activity',
632
634
  group: tool_registry_1.ToolGroup.WRITE,
633
635
  description: createActivityDescription,
636
+ annotations: { readOnlyHint: false, destructiveHint: false },
634
637
  schema: zod_1.z.object({
635
638
  workspaceId: zod_1.z
636
639
  .string()
@@ -970,6 +973,7 @@ exports.updateActivityTool = {
970
973
  name: 'update_activity',
971
974
  group: tool_registry_1.ToolGroup.WRITE,
972
975
  description: updateActivityDescription,
976
+ annotations: { readOnlyHint: false, destructiveHint: false },
973
977
  schema: zod_1.z.object({
974
978
  workspaceId: zod_1.z
975
979
  .string()
@@ -23,6 +23,7 @@ exports.createAppTool = {
23
23
  name: 'create_app',
24
24
  group: tool_registry_1.ToolGroup.PLAYGROUND,
25
25
  description: createAppDescription,
26
+ annotations: { readOnlyHint: false, destructiveHint: false },
26
27
  schema: zod_1.z.object({
27
28
  workspaceId: zod_1.z
28
29
  .string()
@@ -141,6 +142,7 @@ exports.listAppsTool = {
141
142
  name: 'list_apps',
142
143
  group: tool_registry_1.ToolGroup.PLAYGROUND,
143
144
  description: listAppsDescription,
145
+ annotations: { readOnlyHint: true },
144
146
  schema: zod_1.z.object({
145
147
  workspaceId: zod_1.z
146
148
  .string()
@@ -234,6 +236,7 @@ exports.updateAppTool = {
234
236
  name: 'update_app',
235
237
  group: tool_registry_1.ToolGroup.PLAYGROUND,
236
238
  description: updateAppDescription,
239
+ annotations: { readOnlyHint: false, destructiveHint: true },
237
240
  schema: zod_1.z.object({
238
241
  appId: zod_1.z
239
242
  .string()
@@ -347,6 +350,7 @@ exports.removeAppTool = {
347
350
  name: 'remove_app',
348
351
  group: tool_registry_1.ToolGroup.NUCLEAR,
349
352
  description: removeAppDescription,
353
+ annotations: { readOnlyHint: false, destructiveHint: true },
350
354
  schema: zod_1.z.object({
351
355
  appId: zod_1.z
352
356
  .string()
@@ -22,6 +22,7 @@ exports.listTemplatesTool = {
22
22
  name: 'list_templates',
23
23
  group: tool_registry_1.ToolGroup.PLAYGROUND,
24
24
  description: listTemplatesDescription,
25
+ annotations: { readOnlyHint: true },
25
26
  schema: zod_1.z.object({
26
27
  workspaceId: zod_1.z
27
28
  .string()
@@ -128,6 +129,7 @@ exports.createTemplateTool = {
128
129
  name: 'create_template',
129
130
  group: tool_registry_1.ToolGroup.PLAYGROUND,
130
131
  description: createTemplateDescription,
132
+ annotations: { readOnlyHint: false, destructiveHint: false },
131
133
  schema: zod_1.z.object({
132
134
  name: zod_1.z
133
135
  .string()
@@ -212,6 +214,7 @@ exports.installTemplateTool = {
212
214
  name: 'install_template',
213
215
  group: tool_registry_1.ToolGroup.PLAYGROUND,
214
216
  description: installTemplateDescription,
217
+ annotations: { readOnlyHint: false, destructiveHint: true },
215
218
  schema: zod_1.z.object({
216
219
  templateId: zod_1.z
217
220
  .string()
@@ -315,6 +318,7 @@ exports.getTemplateTool = {
315
318
  name: 'get_template',
316
319
  group: tool_registry_1.ToolGroup.PLAYGROUND,
317
320
  description: getTemplateDescription,
321
+ annotations: { readOnlyHint: true },
318
322
  schema: zod_1.z.object({
319
323
  templateId: zod_1.z
320
324
  .string()
@@ -422,6 +426,7 @@ exports.publishTemplateTool = {
422
426
  name: 'publish_template',
423
427
  group: tool_registry_1.ToolGroup.PLAYGROUND,
424
428
  description: publishTemplateDescription,
429
+ annotations: { readOnlyHint: false, destructiveHint: true },
425
430
  schema: zod_1.z.object({
426
431
  productId: zod_1.z
427
432
  .string()
@@ -608,6 +613,7 @@ exports.getProductTool = {
608
613
  name: 'get_product',
609
614
  group: tool_registry_1.ToolGroup.PLAYGROUND,
610
615
  description: getProductDescription,
616
+ annotations: { readOnlyHint: true },
611
617
  schema: zod_1.z.object({
612
618
  productId: zod_1.z
613
619
  .string()
@@ -669,6 +675,7 @@ exports.getProductManifestTool = {
669
675
  name: 'get_product_manifest',
670
676
  group: tool_registry_1.ToolGroup.PLAYGROUND,
671
677
  description: getProductManifestDescription,
678
+ annotations: { readOnlyHint: true },
672
679
  schema: zod_1.z.object({
673
680
  productId: zod_1.z
674
681
  .string()
@@ -723,6 +730,7 @@ const publishAppDescription = `Publish app to Hailer marketplace`;
723
730
  exports.publishAppTool = {
724
731
  name: 'publish_app',
725
732
  group: tool_registry_1.ToolGroup.PLAYGROUND,
733
+ annotations: { readOnlyHint: false, destructiveHint: true },
726
734
  description: publishAppDescription,
727
735
  schema: zod_1.z.object({
728
736
  appId: zod_1.z
@@ -912,6 +920,7 @@ exports.installMarketplaceAppTool = {
912
920
  name: 'install_marketplace_app',
913
921
  group: tool_registry_1.ToolGroup.PLAYGROUND,
914
922
  description: installMarketplaceAppDescription,
923
+ annotations: { readOnlyHint: false, destructiveHint: true },
915
924
  schema: zod_1.z.object({
916
925
  productId: zod_1.z
917
926
  .string()
@@ -18,6 +18,7 @@ exports.addAppMemberTool = {
18
18
  name: 'add_app_member',
19
19
  group: tool_registry_1.ToolGroup.PLAYGROUND,
20
20
  description: addAppMemberDescription,
21
+ annotations: { readOnlyHint: false, destructiveHint: false },
21
22
  schema: zod_1.z.object({
22
23
  appId: zod_1.z
23
24
  .string()
@@ -110,6 +111,7 @@ exports.removeAppMemberTool = {
110
111
  name: 'remove_app_member',
111
112
  group: tool_registry_1.ToolGroup.PLAYGROUND,
112
113
  description: removeAppMemberDescription,
114
+ annotations: { readOnlyHint: false, destructiveHint: true },
113
115
  schema: zod_1.z.object({
114
116
  appId: zod_1.z
115
117
  .string()
@@ -125,6 +125,7 @@ exports.scaffoldHailerAppTool = {
125
125
  name: 'scaffold_hailer_app',
126
126
  group: tool_registry_1.ToolGroup.PLAYGROUND,
127
127
  description: scaffoldHailerAppDescription,
128
+ annotations: { readOnlyHint: false, destructiveHint: false },
128
129
  schema: zod_1.z.object({
129
130
  projectName: zod_1.z.string().min(1).describe("Project folder name"),
130
131
  template: zod_1.z.enum(['react-ts-style', 'react-ts-example', 'react-ts', 'vanilla']).describe("Template to use (react-ts-style recommended - includes Hailer theme)"),
@@ -497,6 +498,7 @@ exports.publishHailerAppTool = {
497
498
  name: 'publish_hailer_app',
498
499
  group: tool_registry_1.ToolGroup.PLAYGROUND,
499
500
  description: publishHailerAppDescription,
501
+ annotations: { readOnlyHint: false, destructiveHint: true },
500
502
  schema: zod_1.z.object({
501
503
  projectDirectory: zod_1.z.string().optional().describe("Path to app project (defaults to DEV_APPS_PATH or current directory)"),
502
504
  appId: zod_1.z.string().optional().describe("App ID to publish to (reads from manifest.json if not provided)"),
@@ -507,15 +509,28 @@ exports.publishHailerAppTool = {
507
509
  async execute(args, context) {
508
510
  const path = await Promise.resolve().then(() => __importStar(require('path')));
509
511
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
510
- // Use provided credentials or fall back to session credentials
511
- const email = args.email || context.email;
512
- const password = args.password || context.password;
512
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
513
+ // Try to read credentials from ~/.hailer-mcp/credentials.json (saved by hailer-mcp setup)
514
+ let savedCredentials = {};
515
+ const credentialsFile = path.join(os.homedir(), '.hailer-mcp', 'credentials.json');
516
+ try {
517
+ if (fs.existsSync(credentialsFile)) {
518
+ savedCredentials = JSON.parse(fs.readFileSync(credentialsFile, 'utf-8'));
519
+ logger.debug('Loaded credentials from ~/.hailer-mcp/credentials.json');
520
+ }
521
+ }
522
+ catch (error) {
523
+ logger.warn('Failed to read credentials file', { error });
524
+ }
525
+ // Priority: args > saved credentials > context
526
+ const email = args.email || savedCredentials.email || context.email;
527
+ const password = args.password || savedCredentials.password || context.password;
513
528
  const { publishToMarket } = args;
514
529
  if (!email || !password) {
515
530
  return {
516
531
  content: [{
517
532
  type: "text",
518
- text: `āŒ **Credentials Required**\n\nNo credentials found in session or parameters.\n\n**Options:**\n1. Provide email/password parameters\n2. Configure CLIENT_CONFIGS in .env.local with credentials`,
533
+ text: `āŒ **Credentials Required**\n\nNo credentials found.\n\n**Run setup first:**\n\`\`\`bash\nhailer-mcp setup\n\`\`\`\n\nOr provide email/password parameters directly.`,
519
534
  }],
520
535
  };
521
536
  }
@@ -649,7 +664,6 @@ exports.publishHailerAppTool = {
649
664
  responseText += `**Project:** ${projectDir}\n`;
650
665
  responseText += `**Marketplace:** ${publishToMarket ? 'Yes (will get targetId)' : 'No'}\n\n`;
651
666
  // Run the SDK publish script using `expect` for interactive automation
652
- const os = await Promise.resolve().then(() => __importStar(require('os')));
653
667
  const result = await new Promise((resolve) => {
654
668
  // Create temp expect script file
655
669
  const tmpDir = os.tmpdir();
@@ -79,6 +79,7 @@ exports.listMyDiscussionsTool = {
79
79
  name: 'list_my_discussions',
80
80
  group: tool_registry_1.ToolGroup.READ,
81
81
  description: listMyDiscussionsDescription,
82
+ annotations: { readOnlyHint: true },
82
83
  schema: zod_1.z.object({}),
83
84
  async execute(args, context) {
84
85
  try {
@@ -241,6 +242,7 @@ exports.fetchDiscussionMessagesTool = {
241
242
  name: 'fetch_discussion_messages',
242
243
  group: tool_registry_1.ToolGroup.READ,
243
244
  description: fetchDiscussionMessagesDescription,
245
+ annotations: { readOnlyHint: true },
244
246
  schema: zod_1.z.object({
245
247
  discussionId: zod_1.z
246
248
  .string()
@@ -403,6 +405,7 @@ exports.fetchPreviousDiscussionMessagesTool = {
403
405
  name: 'fetch_previous_discussion_messages',
404
406
  group: tool_registry_1.ToolGroup.READ,
405
407
  description: fetchPreviousDiscussionMessagesDescription,
408
+ annotations: { readOnlyHint: true },
406
409
  schema: zod_1.z.object({
407
410
  oldestMessageId: zod_1.z
408
411
  .string()
@@ -521,6 +524,7 @@ exports.joinDiscussionTool = {
521
524
  name: 'join_discussion',
522
525
  group: tool_registry_1.ToolGroup.WRITE,
523
526
  description: joinDiscussionDescription,
527
+ annotations: { readOnlyHint: false, destructiveHint: false },
524
528
  schema: zod_1.z.object({
525
529
  activityId: zod_1.z
526
530
  .string()
@@ -801,6 +805,7 @@ exports.leaveDiscussionTool = {
801
805
  name: 'leave_discussion',
802
806
  group: tool_registry_1.ToolGroup.WRITE,
803
807
  description: leaveDiscussionDescription,
808
+ annotations: { readOnlyHint: false, destructiveHint: false },
804
809
  schema: zod_1.z.object({
805
810
  activityId: zod_1.z
806
811
  .string()
@@ -910,6 +915,7 @@ exports.addDiscussionMessageTool = {
910
915
  name: 'add_discussion_message',
911
916
  group: tool_registry_1.ToolGroup.WRITE,
912
917
  description: addDiscussionMessageDescription,
918
+ annotations: { readOnlyHint: false, destructiveHint: false },
913
919
  schema: zod_1.z.object({
914
920
  discussionId: zod_1.z.string().min(24, "Discussion ID must be at least 24 characters").describe("The discussion ID where to post the message"),
915
921
  content: zod_1.z.string().min(1, "Message content cannot be empty").describe("The message text to post"),
@@ -952,6 +958,7 @@ exports.inviteDiscussionMembersTool = {
952
958
  name: 'invite_discussion_members',
953
959
  group: tool_registry_1.ToolGroup.WRITE,
954
960
  description: inviteDiscussionMembersDescription,
961
+ annotations: { readOnlyHint: false, destructiveHint: false },
955
962
  schema: zod_1.z.object({
956
963
  discussionId: zod_1.z
957
964
  .string()
@@ -1103,6 +1110,7 @@ exports.getActivityFromDiscussionTool = {
1103
1110
  name: 'get_activity_from_discussion',
1104
1111
  group: tool_registry_1.ToolGroup.READ,
1105
1112
  description: getActivityFromDiscussionDescription,
1113
+ annotations: { readOnlyHint: true },
1106
1114
  schema: zod_1.z.object({
1107
1115
  discussionId: zod_1.z
1108
1116
  .string()
@@ -62,6 +62,7 @@ exports.uploadFilesTool = {
62
62
  name: 'upload_files',
63
63
  group: tool_registry_1.ToolGroup.WRITE,
64
64
  description: uploadFilesDescription,
65
+ annotations: { readOnlyHint: false, destructiveHint: false },
65
66
  schema: zod_1.z.object({
66
67
  files: zod_1.z.union([
67
68
  zod_1.z.array(zod_1.z.object({
@@ -164,6 +165,7 @@ exports.downloadFileTool = {
164
165
  name: 'download_file',
165
166
  group: tool_registry_1.ToolGroup.READ,
166
167
  description: downloadFileDescription,
168
+ annotations: { readOnlyHint: true },
167
169
  schema: zod_1.z.object({
168
170
  fileId: zod_1.z.string().describe("File ID to download"),
169
171
  savePath: zod_1.z.string().optional().describe("Optional: local path to save file to disk")
@@ -96,6 +96,7 @@ exports.createInsightTool = {
96
96
  name: 'create_insight',
97
97
  group: tool_registry_1.ToolGroup.PLAYGROUND,
98
98
  description: createInsightDescription,
99
+ annotations: { readOnlyHint: false, destructiveHint: true },
99
100
  schema: zod_1.z.object({
100
101
  workspaceId: zod_1.z
101
102
  .string()
@@ -259,6 +260,7 @@ exports.previewInsightTool = {
259
260
  name: 'preview_insight',
260
261
  group: tool_registry_1.ToolGroup.PLAYGROUND,
261
262
  description: previewInsightDescription,
263
+ annotations: { readOnlyHint: true },
262
264
  schema: zod_1.z.object({
263
265
  workspaceId: zod_1.z
264
266
  .string()
@@ -376,6 +378,7 @@ exports.getInsightDataTool = {
376
378
  name: 'get_insight_data',
377
379
  group: tool_registry_1.ToolGroup.PLAYGROUND,
378
380
  description: getInsightDataDescription,
381
+ annotations: { readOnlyHint: true },
379
382
  schema: zod_1.z.object({
380
383
  insightId: zod_1.z
381
384
  .string()
@@ -472,6 +475,7 @@ exports.updateInsightTool = {
472
475
  name: 'update_insight',
473
476
  group: tool_registry_1.ToolGroup.PLAYGROUND,
474
477
  description: updateInsightDescription,
478
+ annotations: { readOnlyHint: false, destructiveHint: true },
475
479
  schema: zod_1.z.object({
476
480
  insightId: zod_1.z
477
481
  .string()
@@ -616,6 +620,7 @@ exports.removeInsightTool = {
616
620
  name: 'remove_insight',
617
621
  group: tool_registry_1.ToolGroup.NUCLEAR,
618
622
  description: removeInsightDescription,
623
+ annotations: { readOnlyHint: false, destructiveHint: true },
619
624
  schema: zod_1.z.object({
620
625
  insightId: zod_1.z
621
626
  .string()
@@ -743,6 +748,7 @@ const listInsightsDescription = `List all insights in workspace`;
743
748
  exports.listInsightsTool = {
744
749
  name: 'list_insights',
745
750
  group: tool_registry_1.ToolGroup.PLAYGROUND,
751
+ annotations: { readOnlyHint: true },
746
752
  description: listInsightsDescription,
747
753
  schema: zod_1.z.object({
748
754
  workspaceId: zod_1.z
@@ -219,6 +219,7 @@ function formatAsChart(data, groupByLabel) {
219
219
  exports.queryMetricTool = {
220
220
  name: 'query_metric',
221
221
  group: tool_registry_1.ToolGroup.READ,
222
+ annotations: { readOnlyHint: true },
222
223
  description: `Query metrics from Victoria Metrics using PromQL. Available metrics:
223
224
  - hailer.activity.create - Activities created (labels: workspace, process, team, userRole)
224
225
  - hailer.message.create - Messages sent (labels: workspace, discussion, userRole)
@@ -329,6 +330,7 @@ exports.listMetricsTool = {
329
330
  name: 'list_metrics',
330
331
  group: tool_registry_1.ToolGroup.READ,
331
332
  description: 'List all available Hailer metrics from Victoria Metrics',
333
+ annotations: { readOnlyHint: true },
332
334
  schema: zod_1.z.object({}),
333
335
  async execute(_args, _context) {
334
336
  try {
@@ -443,6 +445,7 @@ exports.searchWorkspaceForMetricsTool = {
443
445
  group: tool_registry_1.ToolGroup.READ,
444
446
  description: `Search for workspaces by name to get their IDs for metric queries.
445
447
  Use this tool when user mentions a workspace by name (e.g. "Sales Team") to find its ID for filtering metrics.`,
448
+ annotations: { readOnlyHint: true },
446
449
  schema: zod_1.z.object({
447
450
  name: zod_1.z.string().min(3).describe("Workspace name to search (min 3 characters)"),
448
451
  limit: zod_1.z.number().min(1).max(100).optional().default(20).describe("Maximum results to return (default 20, max 100)"),
@@ -495,6 +498,7 @@ exports.searchUserForMetricsTool = {
495
498
  group: tool_registry_1.ToolGroup.READ,
496
499
  description: `Look up user details by ID for metric analysis.
497
500
  Use this after getting user IDs from grouped metric results to see who they are.`,
501
+ annotations: { readOnlyHint: true },
498
502
  schema: zod_1.z.object({
499
503
  userIds: zod_1.z.preprocess((val) => {
500
504
  if (typeof val === 'string') {
@@ -22,6 +22,7 @@ exports.listMyWorkspacesTool = {
22
22
  name: 'list_my_workspaces',
23
23
  group: tool_registry_1.ToolGroup.READ,
24
24
  description: listMyWorkspacesDescription,
25
+ annotations: { readOnlyHint: true },
25
26
  schema: zod_1.z.object({}),
26
27
  async execute(_args, context) {
27
28
  logger.debug('Listing user workspaces', {
@@ -41,7 +42,9 @@ exports.listMyWorkspacesTool = {
41
42
  .map(([id, ws]) => {
42
43
  const isCurrent = id === currentWorkspaceId;
43
44
  const marker = isCurrent ? ' ← current' : '';
44
- return `• **${ws.name}**${marker}\n - ID: \`${id}\``;
45
+ const role = context.workspaceRoles[id] || 'guest';
46
+ const roleEmoji = role === 'owner' ? 'šŸ‘‘' : role === 'admin' ? 'āš™ļø' : role === 'member' ? 'šŸ‘¤' : 'šŸ‘ļø';
47
+ return `• **${ws.name}**${marker}\n - ID: \`${id}\`\n - Role: ${roleEmoji} ${role}`;
45
48
  })
46
49
  .join('\n\n');
47
50
  const hint = workspaceList.length > 1
@@ -65,6 +68,7 @@ exports.searchWorkspaceUsersTool = {
65
68
  name: 'search_workspace_users',
66
69
  group: tool_registry_1.ToolGroup.READ,
67
70
  description: searchWorkspaceUsersDescription,
71
+ annotations: { readOnlyHint: true },
68
72
  schema: zod_1.z.object({
69
73
  query: zod_1.z
70
74
  .string()
@@ -132,6 +136,7 @@ exports.getWorkspaceBalanceTool = {
132
136
  name: 'get_workspace_balance',
133
137
  group: tool_registry_1.ToolGroup.READ,
134
138
  description: getWorkspaceBalanceDescription,
139
+ annotations: { readOnlyHint: true },
135
140
  schema: zod_1.z.object({
136
141
  workspaceId: zod_1.z
137
142
  .string()
@@ -57,6 +57,7 @@ exports.getWorkflowSchemaTool = {
57
57
  name: 'get_workflow_schema',
58
58
  group: tool_registry_1.ToolGroup.READ,
59
59
  description: getWorkflowSchemaDescription,
60
+ annotations: { readOnlyHint: true },
60
61
  schema: zod_1.z.object({
61
62
  workflowId: zod_1.z.string().describe("Workflow ID to get schema from"),
62
63
  phaseId: zod_1.z.string().describe("Phase ID to get schema from (use list_workflow_phases to get available phases)"),
@@ -181,6 +182,7 @@ exports.listWorkflowPhasesTool = {
181
182
  name: 'list_workflow_phases',
182
183
  group: tool_registry_1.ToolGroup.READ,
183
184
  description: listWorkflowPhasesDescription,
185
+ annotations: { readOnlyHint: true },
184
186
  schema: zod_1.z.object({
185
187
  workflowId: zod_1.z.string().describe("Workflow ID to get phases from"),
186
188
  }),
@@ -259,6 +261,7 @@ exports.listWorkflowsTool = {
259
261
  name: 'list_workflows',
260
262
  group: tool_registry_1.ToolGroup.READ,
261
263
  description: listWorkflowsDescription,
264
+ annotations: { readOnlyHint: true },
262
265
  schema: zod_1.z.object({
263
266
  workspace: zod_1.z.string().optional().describe("Optional workspace ID or name"),
264
267
  includeRelationships: zod_1.z.coerce.boolean().optional().default(true).describe("Show ActivityLink relationships between workflows"),
@@ -421,6 +424,7 @@ exports.installWorkflowTool = {
421
424
  name: 'install_workflow',
422
425
  group: tool_registry_1.ToolGroup.PLAYGROUND,
423
426
  description: installWorkflowDescription,
427
+ annotations: { readOnlyHint: false, destructiveHint: true },
424
428
  schema: installWorkflowSchema,
425
429
  async execute(args, context) {
426
430
  logger.debug('Installing workflow', {
@@ -560,6 +564,7 @@ exports.removeWorkflowTool = {
560
564
  name: 'remove_workflow',
561
565
  group: tool_registry_1.ToolGroup.NUCLEAR,
562
566
  description: removeWorkflowDescription,
567
+ annotations: { readOnlyHint: false, destructiveHint: true },
563
568
  schema: removeWorkflowSchema,
564
569
  async execute(args, context) {
565
570
  logger.debug('Removing workflow', {
@@ -710,6 +715,7 @@ exports.updateWorkflowFieldTool = {
710
715
  name: 'update_workflow_field',
711
716
  group: tool_registry_1.ToolGroup.PLAYGROUND,
712
717
  description: updateWorkflowFieldDescription,
718
+ annotations: { readOnlyHint: false, destructiveHint: true },
713
719
  schema: updateWorkflowFieldSchema,
714
720
  async execute(args, context) {
715
721
  logger.debug('Updating workflow field', {
@@ -838,6 +844,7 @@ exports.testFunctionFieldTool = {
838
844
  name: 'test_function_field',
839
845
  group: tool_registry_1.ToolGroup.PLAYGROUND,
840
846
  description: testFunctionFieldDescription,
847
+ annotations: { readOnlyHint: false, destructiveHint: true },
841
848
  schema: testFunctionFieldSchema,
842
849
  async execute(args, context) {
843
850
  logger.debug('Testing function field', {
@@ -1019,6 +1026,7 @@ exports.listWorkflowsMinimalTool = {
1019
1026
  name: 'list_workflows_minimal',
1020
1027
  group: tool_registry_1.ToolGroup.PLAYGROUND,
1021
1028
  description: listWorkflowsMinimalDescription,
1029
+ annotations: { readOnlyHint: true },
1022
1030
  schema: listWorkflowsMinimalSchema,
1023
1031
  async execute(args, context) {
1024
1032
  logger.debug('Listing workflows (minimal)', {
@@ -1131,6 +1139,7 @@ exports.countActivitiesTool = {
1131
1139
  name: 'count_activities',
1132
1140
  group: tool_registry_1.ToolGroup.PLAYGROUND,
1133
1141
  description: countActivitiesDescription,
1142
+ annotations: { readOnlyHint: true },
1134
1143
  schema: countActivitiesSchema,
1135
1144
  async execute(args, context) {
1136
1145
  logger.debug('Counting activities', {
@@ -1234,6 +1243,7 @@ exports.updateWorkflowPhaseTool = {
1234
1243
  name: 'update_workflow_phase',
1235
1244
  group: tool_registry_1.ToolGroup.PLAYGROUND,
1236
1245
  description: updateWorkflowPhaseDescription,
1246
+ annotations: { readOnlyHint: false, destructiveHint: true },
1237
1247
  schema: updateWorkflowPhaseSchema,
1238
1248
  async execute(args, context) {
1239
1249
  logger.debug('Updating workflow phase', {
@@ -12,5 +12,6 @@ export { transformActivity, transformActivities, transformFields, transformField
12
12
  export { textResponse, errorResponse, successResponse, jsonResponse, paginatedResponse, listResponse, getErrorMessage, withErrorHandling, isErrorResponse } from './response-builder';
13
13
  export { normalizePagination, calculatePaginationMeta, formatPaginationText } from './pagination';
14
14
  export type { PaginationMeta, PaginationOptions } from './pagination';
15
+ export { deriveUserRole, getAllowedGroups, findCurrentUserMember, extractWorkspaceRoles, getAllowedGroupsForWorkspace, getMaxRole, checkWorkspaceAccess } from './role-utils';
15
16
  export type * from './types';
16
17
  //# sourceMappingURL=index.d.ts.map
@@ -4,7 +4,7 @@
4
4
  * Provides centralized access to all utility functions and types
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.formatPaginationText = exports.calculatePaginationMeta = exports.normalizePagination = exports.isErrorResponse = exports.withErrorHandling = exports.getErrorMessage = exports.listResponse = exports.paginatedResponse = exports.jsonResponse = exports.successResponse = exports.errorResponse = exports.textResponse = exports.formatActivityListResponse = exports.formatUserName = exports.formatTimestamp = exports.transformWorkflowFields = exports.transformPhases = exports.transformWorkflow = exports.findFieldByKey = exports.transformFieldValue = exports.transformFields = exports.transformActivities = exports.transformActivity = exports.HailerApiClient = exports.createSuccessResponse = exports.createErrorResponse = exports.handleApiResponse = exports.makeApiCall = exports.HailerApiError = exports.LogTag = exports.LogLevel = exports.logger = exports.createLogger = void 0;
7
+ exports.checkWorkspaceAccess = exports.getMaxRole = exports.getAllowedGroupsForWorkspace = exports.extractWorkspaceRoles = exports.findCurrentUserMember = exports.getAllowedGroups = exports.deriveUserRole = exports.formatPaginationText = exports.calculatePaginationMeta = exports.normalizePagination = exports.isErrorResponse = exports.withErrorHandling = exports.getErrorMessage = exports.listResponse = exports.paginatedResponse = exports.jsonResponse = exports.successResponse = exports.errorResponse = exports.textResponse = exports.formatActivityListResponse = exports.formatUserName = exports.formatTimestamp = exports.transformWorkflowFields = exports.transformPhases = exports.transformWorkflow = exports.findFieldByKey = exports.transformFieldValue = exports.transformFields = exports.transformActivities = exports.transformActivity = exports.HailerApiClient = exports.createSuccessResponse = exports.createErrorResponse = exports.handleApiResponse = exports.makeApiCall = exports.HailerApiError = exports.LogTag = exports.LogLevel = exports.logger = exports.createLogger = void 0;
8
8
  // Logging utilities
9
9
  var logger_1 = require("../../lib/logger");
10
10
  Object.defineProperty(exports, "createLogger", { enumerable: true, get: function () { return logger_1.createLogger; } });
@@ -50,4 +50,13 @@ var pagination_1 = require("./pagination");
50
50
  Object.defineProperty(exports, "normalizePagination", { enumerable: true, get: function () { return pagination_1.normalizePagination; } });
51
51
  Object.defineProperty(exports, "calculatePaginationMeta", { enumerable: true, get: function () { return pagination_1.calculatePaginationMeta; } });
52
52
  Object.defineProperty(exports, "formatPaginationText", { enumerable: true, get: function () { return pagination_1.formatPaginationText; } });
53
+ // Role-based access control
54
+ var role_utils_1 = require("./role-utils");
55
+ Object.defineProperty(exports, "deriveUserRole", { enumerable: true, get: function () { return role_utils_1.deriveUserRole; } });
56
+ Object.defineProperty(exports, "getAllowedGroups", { enumerable: true, get: function () { return role_utils_1.getAllowedGroups; } });
57
+ Object.defineProperty(exports, "findCurrentUserMember", { enumerable: true, get: function () { return role_utils_1.findCurrentUserMember; } });
58
+ Object.defineProperty(exports, "extractWorkspaceRoles", { enumerable: true, get: function () { return role_utils_1.extractWorkspaceRoles; } });
59
+ Object.defineProperty(exports, "getAllowedGroupsForWorkspace", { enumerable: true, get: function () { return role_utils_1.getAllowedGroupsForWorkspace; } });
60
+ Object.defineProperty(exports, "getMaxRole", { enumerable: true, get: function () { return role_utils_1.getMaxRole; } });
61
+ Object.defineProperty(exports, "checkWorkspaceAccess", { enumerable: true, get: function () { return role_utils_1.checkWorkspaceAccess; } });
53
62
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Role-Based Access Control Utilities
3
+ *
4
+ * Derives user role from workspace member flags and maps roles to ToolGroups.
5
+ * Used by UserContextCache to determine tool access at context creation time.
6
+ */
7
+ import { ToolGroup } from '../tool-registry';
8
+ import { UserRole, WorkspaceMember, WorkspaceInfo } from './types';
9
+ /**
10
+ * Derive user role from workspace member flags
11
+ * Priority: owner > admin > guest > member
12
+ *
13
+ * @param member - Workspace member from v2.core.init
14
+ * @returns UserRole - 'owner' | 'admin' | 'guest' | 'member'
15
+ */
16
+ export declare function deriveUserRole(member: WorkspaceMember): UserRole;
17
+ /**
18
+ * Map user role to allowed ToolGroups
19
+ *
20
+ * @param role - User role derived from workspace member
21
+ * @param enableNuclear - Optional override to disable NUCLEAR even for owners
22
+ * @returns Array of ToolGroups the user can access
23
+ */
24
+ export declare function getAllowedGroups(role: UserRole, enableNuclear?: boolean): ToolGroup[];
25
+ /**
26
+ * Find current user in workspace members array
27
+ *
28
+ * @param members - Array of workspace members from init.network.members
29
+ * @param currentUserId - Current user's ID
30
+ * @returns WorkspaceMember if found, undefined otherwise
31
+ */
32
+ export declare function findCurrentUserMember(members: WorkspaceMember[], currentUserId: string): WorkspaceMember | undefined;
33
+ /**
34
+ * Extract user roles from all workspaces
35
+ * Returns a map of workspaceId → UserRole
36
+ *
37
+ * @param networks - Record of workspace ID to WorkspaceInfo from init.networks
38
+ * @param currentUserId - Current user's ID
39
+ * @returns Record mapping workspace IDs to UserRoles
40
+ */
41
+ export declare function extractWorkspaceRoles(networks: Record<string, WorkspaceInfo>, currentUserId: string): Record<string, UserRole>;
42
+ /**
43
+ * Get allowed groups for a specific workspace
44
+ *
45
+ * @param workspaceRoles - Map of workspace IDs to UserRoles
46
+ * @param workspaceId - Target workspace ID
47
+ * @param enableNuclear - Optional override to disable NUCLEAR even for owners
48
+ * @returns Array of ToolGroups the user can access in the specified workspace
49
+ */
50
+ export declare function getAllowedGroupsForWorkspace(workspaceRoles: Record<string, UserRole>, workspaceId: string, enableNuclear?: boolean): ToolGroup[];
51
+ /**
52
+ * Get the highest role across all workspaces
53
+ * Used to determine which tools to show at startup (max potential access)
54
+ *
55
+ * @param workspaceRoles - Map of workspace IDs to UserRoles
56
+ * @returns Highest UserRole across all workspaces
57
+ */
58
+ export declare function getMaxRole(workspaceRoles: Record<string, UserRole>): UserRole;
59
+ /**
60
+ * Check if user has access to a specific ToolGroup in a workspace
61
+ * Used for runtime permission validation when tools are called with workspaceId
62
+ *
63
+ * @param workspaceRoles - Map of workspace IDs to UserRoles
64
+ * @param currentWorkspaceId - Current default workspace ID
65
+ * @param targetWorkspaceId - Target workspace ID (or undefined to use current)
66
+ * @param requiredGroup - ToolGroup required for the operation
67
+ * @param enableNuclear - Optional override to disable NUCLEAR even for owners
68
+ * @returns Object with allowed boolean and optional reason string
69
+ */
70
+ export declare function checkWorkspaceAccess(workspaceRoles: Record<string, UserRole>, currentWorkspaceId: string, targetWorkspaceId: string | undefined, requiredGroup: ToolGroup, enableNuclear?: boolean): {
71
+ allowed: boolean;
72
+ reason?: string;
73
+ };
74
+ //# sourceMappingURL=role-utils.d.ts.map
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * Role-Based Access Control Utilities
4
+ *
5
+ * Derives user role from workspace member flags and maps roles to ToolGroups.
6
+ * Used by UserContextCache to determine tool access at context creation time.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.deriveUserRole = deriveUserRole;
10
+ exports.getAllowedGroups = getAllowedGroups;
11
+ exports.findCurrentUserMember = findCurrentUserMember;
12
+ exports.extractWorkspaceRoles = extractWorkspaceRoles;
13
+ exports.getAllowedGroupsForWorkspace = getAllowedGroupsForWorkspace;
14
+ exports.getMaxRole = getMaxRole;
15
+ exports.checkWorkspaceAccess = checkWorkspaceAccess;
16
+ const tool_registry_1 = require("../tool-registry");
17
+ /**
18
+ * Derive user role from workspace member flags
19
+ * Priority: owner > admin > guest > member
20
+ *
21
+ * @param member - Workspace member from v2.core.init
22
+ * @returns UserRole - 'owner' | 'admin' | 'guest' | 'member'
23
+ */
24
+ function deriveUserRole(member) {
25
+ if (member.owner)
26
+ return 'owner';
27
+ if (member.admin)
28
+ return 'admin';
29
+ if (member.guest)
30
+ return 'guest';
31
+ return 'member';
32
+ }
33
+ /**
34
+ * Map user role to allowed ToolGroups
35
+ *
36
+ * @param role - User role derived from workspace member
37
+ * @param enableNuclear - Optional override to disable NUCLEAR even for owners
38
+ * @returns Array of ToolGroups the user can access
39
+ */
40
+ function getAllowedGroups(role, enableNuclear = true) {
41
+ switch (role) {
42
+ case 'owner':
43
+ return enableNuclear
44
+ ? [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND, tool_registry_1.ToolGroup.NUCLEAR]
45
+ : [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND];
46
+ case 'admin':
47
+ return [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND];
48
+ case 'member':
49
+ return [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE];
50
+ case 'guest':
51
+ return [tool_registry_1.ToolGroup.READ];
52
+ }
53
+ }
54
+ /**
55
+ * Find current user in workspace members array
56
+ *
57
+ * @param members - Array of workspace members from init.network.members
58
+ * @param currentUserId - Current user's ID
59
+ * @returns WorkspaceMember if found, undefined otherwise
60
+ */
61
+ function findCurrentUserMember(members, currentUserId) {
62
+ return members.find(m => m.uid === currentUserId);
63
+ }
64
+ /**
65
+ * Extract user roles from all workspaces
66
+ * Returns a map of workspaceId → UserRole
67
+ *
68
+ * @param networks - Record of workspace ID to WorkspaceInfo from init.networks
69
+ * @param currentUserId - Current user's ID
70
+ * @returns Record mapping workspace IDs to UserRoles
71
+ */
72
+ function extractWorkspaceRoles(networks, currentUserId) {
73
+ const roles = {};
74
+ for (const [wsId, network] of Object.entries(networks)) {
75
+ const members = (network.members || []);
76
+ const member = findCurrentUserMember(members, currentUserId);
77
+ roles[wsId] = member ? deriveUserRole(member) : 'guest';
78
+ }
79
+ return roles;
80
+ }
81
+ /**
82
+ * Get allowed groups for a specific workspace
83
+ *
84
+ * @param workspaceRoles - Map of workspace IDs to UserRoles
85
+ * @param workspaceId - Target workspace ID
86
+ * @param enableNuclear - Optional override to disable NUCLEAR even for owners
87
+ * @returns Array of ToolGroups the user can access in the specified workspace
88
+ */
89
+ function getAllowedGroupsForWorkspace(workspaceRoles, workspaceId, enableNuclear = true) {
90
+ const role = workspaceRoles[workspaceId] || 'guest';
91
+ return getAllowedGroups(role, enableNuclear);
92
+ }
93
+ /**
94
+ * Get the highest role across all workspaces
95
+ * Used to determine which tools to show at startup (max potential access)
96
+ *
97
+ * @param workspaceRoles - Map of workspace IDs to UserRoles
98
+ * @returns Highest UserRole across all workspaces
99
+ */
100
+ function getMaxRole(workspaceRoles) {
101
+ const roleOrder = ['guest', 'member', 'admin', 'owner'];
102
+ let maxRole = 'guest';
103
+ for (const role of Object.values(workspaceRoles)) {
104
+ if (roleOrder.indexOf(role) > roleOrder.indexOf(maxRole)) {
105
+ maxRole = role;
106
+ }
107
+ }
108
+ return maxRole;
109
+ }
110
+ /**
111
+ * Get minimum role required for a ToolGroup
112
+ * Used for error messages
113
+ */
114
+ function getRequiredRoleForGroup(group) {
115
+ switch (group) {
116
+ case tool_registry_1.ToolGroup.READ:
117
+ return 'guest';
118
+ case tool_registry_1.ToolGroup.WRITE:
119
+ return 'member';
120
+ case tool_registry_1.ToolGroup.PLAYGROUND:
121
+ return 'admin';
122
+ case tool_registry_1.ToolGroup.NUCLEAR:
123
+ return 'owner';
124
+ default:
125
+ return 'owner';
126
+ }
127
+ }
128
+ /**
129
+ * Check if user has access to a specific ToolGroup in a workspace
130
+ * Used for runtime permission validation when tools are called with workspaceId
131
+ *
132
+ * @param workspaceRoles - Map of workspace IDs to UserRoles
133
+ * @param currentWorkspaceId - Current default workspace ID
134
+ * @param targetWorkspaceId - Target workspace ID (or undefined to use current)
135
+ * @param requiredGroup - ToolGroup required for the operation
136
+ * @param enableNuclear - Optional override to disable NUCLEAR even for owners
137
+ * @returns Object with allowed boolean and optional reason string
138
+ */
139
+ function checkWorkspaceAccess(workspaceRoles, currentWorkspaceId, targetWorkspaceId, requiredGroup, enableNuclear = true) {
140
+ const effectiveWsId = targetWorkspaceId || currentWorkspaceId;
141
+ const role = workspaceRoles[effectiveWsId] || 'guest';
142
+ const allowedGroups = getAllowedGroups(role, enableNuclear);
143
+ if (!allowedGroups.includes(requiredGroup)) {
144
+ return {
145
+ allowed: false,
146
+ reason: `Insufficient permissions in workspace '${effectiveWsId.slice(-6)}'. Your role '${role}' doesn't have access to ${requiredGroup} tools. Required: ${getRequiredRoleForGroup(requiredGroup)} or higher.`
147
+ };
148
+ }
149
+ return { allowed: true };
150
+ }
151
+ //# sourceMappingURL=role-utils.js.map
@@ -2,6 +2,29 @@
2
2
  * Shared type definitions for Hailer MCP Server
3
3
  * Consolidates interfaces used across multiple files
4
4
  */
5
+ /**
6
+ * User role in workspace (derived from member flags)
7
+ * Used to determine which ToolGroups are available to the user
8
+ */
9
+ export type UserRole = 'guest' | 'member' | 'admin' | 'owner';
10
+ /**
11
+ * Workspace member from v2.core.init response
12
+ * Contains role flags that determine user permissions
13
+ *
14
+ * Schema reference: hailer-api/src/validation/sharedSchemas.ts (validWorkspaceMemberSchema)
15
+ */
16
+ export interface WorkspaceMember {
17
+ uid: string;
18
+ title?: string;
19
+ owner?: boolean;
20
+ admin?: boolean;
21
+ guest?: boolean;
22
+ inviter?: boolean;
23
+ feedAdmin?: boolean;
24
+ customRole?: string;
25
+ joined: number;
26
+ fields?: Record<string, string | string[] | null>;
27
+ }
5
28
  export type { CleanActivity, FieldValue, WorkflowInfo, PhaseInfo, FieldInfo, UserInfo, } from './data-transformers';
6
29
  export interface HailerField {
7
30
  data: any[];
@@ -173,7 +196,7 @@ export interface WorkspaceInfo {
173
196
  _id: string;
174
197
  name: string;
175
198
  description?: string;
176
- members?: string[];
199
+ members?: WorkspaceMember[];
177
200
  settings?: Record<string, any>;
178
201
  }
179
202
  export interface SignalData {
@@ -32,15 +32,21 @@ async function startStdioServer(toolRegistry) {
32
32
  name: 'hailer-mcp-server',
33
33
  version: '1.0.0',
34
34
  });
35
- // Get tool definitions (filtered by allowed groups, excluding NUCLEAR unless enabled)
35
+ // Get user context to determine role-based tool access
36
+ const userContext = await UserContextCache_1.UserContextCache.getContext(apiKey);
37
+ // Show ALL tools - permission checks happen at runtime via checkWorkspaceAccess()
38
+ // ENABLE_NUCLEAR_TOOLS still controls whether NUCLEAR tools are available at all
36
39
  const allowedGroups = config_1.environment.ENABLE_NUCLEAR_TOOLS
37
40
  ? [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND, tool_registry_1.ToolGroup.NUCLEAR]
38
41
  : [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND];
39
42
  // Get tools with their original Zod schemas - MCP SDK requires Zod, not JSON Schema
40
43
  const tools = toolRegistry.getToolsWithZodSchemas({ allowedGroups });
41
- logger.info('Registering tools for stdio server', {
44
+ logger.info('Tools registered (runtime permission checks apply)', {
45
+ currentWorkspaceId: userContext.currentWorkspaceId,
46
+ currentRole: userContext.workspaceRoles[userContext.currentWorkspaceId] || 'guest',
42
47
  toolCount: tools.length,
43
- allowedGroups,
48
+ workspaceCount: Object.keys(userContext.workspaceRoles).length,
49
+ nuclearEnabled: config_1.environment.ENABLE_NUCLEAR_TOOLS
44
50
  });
45
51
  // Register each tool with the MCP server using Zod schemas
46
52
  for (const tool of tools) {
package/manifest.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "mcpb_version": "0.1",
3
+ "name": "hailer-mcp",
4
+ "version": "1.0.29",
5
+ "display_name": "Hailer MCP",
6
+ "description": "Connect Claude to Hailer workspaces for workflow management, activity tracking, discussions, and insights",
7
+ "author": {
8
+ "name": "Hailer",
9
+ "url": "https://hailer.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/hailer/hailer-mcp"
14
+ },
15
+ "server": {
16
+ "type": "stdio",
17
+ "entry_point": "dist/app.js",
18
+ "mcp_config": {
19
+ "command": "node",
20
+ "args": ["${serverPath}/dist/app.js"]
21
+ }
22
+ },
23
+ "user_config": [
24
+ {
25
+ "id": "mcp_client_api_key",
26
+ "name": "Client API Key",
27
+ "description": "Identifier for this client (any string, e.g. 'claude-desktop')",
28
+ "type": "string",
29
+ "required": true,
30
+ "default": "claude-desktop",
31
+ "env_var": "MCP_CLIENT_API_KEY"
32
+ },
33
+ {
34
+ "id": "hailer_email",
35
+ "name": "Hailer Email",
36
+ "description": "Your Hailer account email",
37
+ "type": "string",
38
+ "required": true,
39
+ "env_var": "HAILER_EMAIL"
40
+ },
41
+ {
42
+ "id": "hailer_password",
43
+ "name": "Hailer Password",
44
+ "description": "Your Hailer account password",
45
+ "type": "string",
46
+ "required": true,
47
+ "sensitive": true,
48
+ "env_var": "HAILER_PASSWORD"
49
+ },
50
+ {
51
+ "id": "hailer_api_url",
52
+ "name": "API URL",
53
+ "description": "Hailer API URL (default: https://api.hailer.com)",
54
+ "type": "string",
55
+ "required": false,
56
+ "default": "https://api.hailer.com",
57
+ "env_var": "HAILER_API_URL"
58
+ }
59
+ ],
60
+ "categories": ["productivity", "workflow", "collaboration"],
61
+ "keywords": ["hailer", "workflow", "activity", "discussion", "insight", "crm"]
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hailer/mcp",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "config": {
5
5
  "docker": {
6
6
  "registry": "registry.gitlab.com/hailer-repos/hailer-mcp"