fastmode-mcp 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,27 +138,55 @@ The MCP server automatically downloads prebuilt binaries for your platform durin
138
138
 
139
139
  Add to `~/.claude/settings.json`:
140
140
 
141
+ **macOS/Linux:**
141
142
  ```json
142
143
  {
143
144
  "mcpServers": {
144
145
  "fastmode": {
145
- "command": "npx",
146
- "args": ["-y", "fastmode-mcp"]
146
+ "command": "/bin/zsh",
147
+ "args": ["-l", "-c", "npx -y fastmode-mcp"]
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ **Windows:**
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "fastmode": {
158
+ "command": "cmd",
159
+ "args": ["/c", "npx -y fastmode-mcp"]
147
160
  }
148
161
  }
149
162
  }
150
163
  ```
151
164
 
165
+ > **Why the shell wrapper?** VS Code extensions don't inherit your shell's PATH, so if you use nvm, volta, or homebrew for Node.js, the simple `npx` command won't be found. The shell wrapper loads your profile first.
166
+
152
167
  ### For Cursor
153
168
 
154
169
  Add to your Cursor MCP settings (`~/.cursor/mcp.json` or project-level `.cursor/mcp.json`):
155
170
 
171
+ **macOS/Linux:**
156
172
  ```json
157
173
  {
158
174
  "mcpServers": {
159
175
  "fastmode": {
160
- "command": "npx",
161
- "args": ["-y", "fastmode-mcp"]
176
+ "command": "/bin/zsh",
177
+ "args": ["-l", "-c", "npx -y fastmode-mcp"]
178
+ }
179
+ }
180
+ }
181
+ ```
182
+
183
+ **Windows:**
184
+ ```json
185
+ {
186
+ "mcpServers": {
187
+ "fastmode": {
188
+ "command": "cmd",
189
+ "args": ["/c", "npx -y fastmode-mcp"]
162
190
  }
163
191
  }
164
192
  }
@@ -194,9 +222,59 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
194
222
  }
195
223
  ```
196
224
 
225
+ ### For Antigravity IDE
226
+
227
+ Add to `~/.gemini/antigravity/mcp_config.json`:
228
+
229
+ ```json
230
+ {
231
+ "mcpServers": {
232
+ "fastmode": {
233
+ "command": "/bin/zsh",
234
+ "args": ["-l", "-c", "npx -y fastmode-mcp"]
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ > **Note:** Antigravity IDE requires the shell wrapper method because it doesn't load your shell's PATH.
241
+
242
+ ### For Other IDEs
243
+
244
+ If your IDE isn't listed above, find your MCP config file and use the shell wrapper method:
245
+
246
+ **macOS/Linux:**
247
+ ```json
248
+ {
249
+ "mcpServers": {
250
+ "fastmode": {
251
+ "command": "/bin/zsh",
252
+ "args": ["-l", "-c", "npx -y fastmode-mcp"]
253
+ }
254
+ }
255
+ }
256
+ ```
257
+
258
+ **Windows:**
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "fastmode": {
263
+ "command": "cmd",
264
+ "args": ["/c", "npx -y fastmode-mcp"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Common config file locations:
271
+ - `~/.gemini/antigravity/mcp_config.json` (Antigravity)
272
+ - `~/.config/your-ide/mcp.json`
273
+ - `~/.your-ide/mcp_config.json`
274
+
197
275
  ### Troubleshooting
198
276
 
199
- If the MCP server doesn't start (especially with custom Node.js installations like nvm, volta, or homebrew):
277
+ If the MCP server doesn't start (common with custom Node.js installations like nvm/volta/homebrew, or IDEs that don't load your shell's PATH):
200
278
 
201
279
  **Option 1: Use shell wrapper (macOS/Linux)**
202
280
 
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ const sync_schema_1 = require("./tools/sync-schema");
51
51
  const generate_samples_1 = require("./tools/generate-samples");
52
52
  const get_started_1 = require("./tools/get-started");
53
53
  const cms_items_1 = require("./tools/cms-items");
54
+ const portal_clients_1 = require("./tools/portal-clients");
54
55
  // Server instructions - embedded in tool descriptions since MCP SDK doesn't have a dedicated field
55
56
  // The get_started tool description acts as the primary entry point guidance for agents
56
57
  const server = new index_js_1.Server({
@@ -73,8 +74,8 @@ const TOOLS = [
73
74
  properties: {
74
75
  intent: {
75
76
  type: 'string',
76
- enum: ['explore', 'add_content', 'update_schema', 'convert', 'deploy'],
77
- description: 'What you want to do: explore (see projects), add_content (create/edit items), update_schema (add collections/fields), convert (new website), deploy (push changes)',
77
+ enum: ['explore', 'add_content', 'update_schema', 'convert', 'deploy', 'manage_clients'],
78
+ description: 'What you want to do: explore (see projects), add_content (create/edit items), update_schema (add collections/fields), convert (new website), deploy (push changes), manage_clients (invite/manage portal clients)',
78
79
  },
79
80
  projectId: {
80
81
  type: 'string',
@@ -540,6 +541,129 @@ const TOOLS = [
540
541
  required: ['projectId', 'collectionSlug'],
541
542
  },
542
543
  },
544
+ // Portal Client Management
545
+ {
546
+ name: 'invite_client',
547
+ description: 'Invite a client to the project portal. Sends an invitation with a unique link. The client creates a password and gets access to manage content. Portal is auto-enabled on the project if not already active. Default permissions: cms.read, cms.write, editor, forms.read.',
548
+ inputSchema: {
549
+ type: 'object',
550
+ properties: {
551
+ projectId: {
552
+ type: 'string',
553
+ description: 'Project ID (UUID) or project name.',
554
+ },
555
+ email: {
556
+ type: 'string',
557
+ description: 'Client email address to invite.',
558
+ },
559
+ name: {
560
+ type: 'string',
561
+ description: 'Optional: Client name for the invitation.',
562
+ },
563
+ permissions: {
564
+ type: 'array',
565
+ items: { type: 'string' },
566
+ description: 'Optional: Array of permissions. Available: cms.read, cms.write, editor, forms.read, dns, api, notifications, billing. Defaults to [cms.read, cms.write, editor, forms.read] if not specified.',
567
+ },
568
+ },
569
+ required: ['projectId', 'email'],
570
+ },
571
+ },
572
+ {
573
+ name: 'list_clients',
574
+ description: 'List all portal clients (users) who have access to this project. Shows their email, name, permissions, and last login.',
575
+ inputSchema: {
576
+ type: 'object',
577
+ properties: {
578
+ projectId: {
579
+ type: 'string',
580
+ description: 'Project ID (UUID) or project name.',
581
+ },
582
+ },
583
+ required: ['projectId'],
584
+ },
585
+ },
586
+ {
587
+ name: 'list_invitations',
588
+ description: 'List pending portal invitations for this project. Shows invitations that have been sent but not yet accepted.',
589
+ inputSchema: {
590
+ type: 'object',
591
+ properties: {
592
+ projectId: {
593
+ type: 'string',
594
+ description: 'Project ID (UUID) or project name.',
595
+ },
596
+ },
597
+ required: ['projectId'],
598
+ },
599
+ },
600
+ {
601
+ name: 'update_client_permissions',
602
+ description: 'Update the permissions for an existing portal client. Use list_clients to get the access ID. Available permissions: cms.read, cms.write, editor, forms.read, dns, api, notifications, billing.',
603
+ inputSchema: {
604
+ type: 'object',
605
+ properties: {
606
+ projectId: {
607
+ type: 'string',
608
+ description: 'Project ID (UUID) or project name.',
609
+ },
610
+ accessId: {
611
+ type: 'string',
612
+ description: 'The access ID of the client (from list_clients).',
613
+ },
614
+ permissions: {
615
+ type: 'array',
616
+ items: { type: 'string' },
617
+ description: 'The new set of permissions to assign. This replaces ALL existing permissions.',
618
+ },
619
+ },
620
+ required: ['projectId', 'accessId', 'permissions'],
621
+ },
622
+ },
623
+ {
624
+ name: 'revoke_client_access',
625
+ description: 'Revoke a client\'s portal access. REQUIRES explicit user confirmation before calling. Ask the user to confirm first. The client will no longer be able to access the portal.',
626
+ inputSchema: {
627
+ type: 'object',
628
+ properties: {
629
+ projectId: {
630
+ type: 'string',
631
+ description: 'Project ID (UUID) or project name.',
632
+ },
633
+ accessId: {
634
+ type: 'string',
635
+ description: 'The access ID of the client to revoke (from list_clients).',
636
+ },
637
+ confirmRevoke: {
638
+ type: 'boolean',
639
+ description: 'Must be true. Only set this after the user has explicitly confirmed they want to revoke access.',
640
+ },
641
+ },
642
+ required: ['projectId', 'accessId', 'confirmRevoke'],
643
+ },
644
+ },
645
+ {
646
+ name: 'cancel_invitation',
647
+ description: 'Cancel a pending portal invitation. REQUIRES explicit user confirmation before calling. The invitation link will no longer work.',
648
+ inputSchema: {
649
+ type: 'object',
650
+ properties: {
651
+ projectId: {
652
+ type: 'string',
653
+ description: 'Project ID (UUID) or project name.',
654
+ },
655
+ invitationId: {
656
+ type: 'string',
657
+ description: 'The invitation ID to cancel (from list_invitations).',
658
+ },
659
+ confirmCancel: {
660
+ type: 'boolean',
661
+ description: 'Must be true. Only set this after the user has explicitly confirmed they want to cancel the invitation.',
662
+ },
663
+ },
664
+ required: ['projectId', 'invitationId', 'confirmCancel'],
665
+ },
666
+ },
543
667
  ];
544
668
  // Handle list tools request
545
669
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
@@ -662,6 +786,45 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
662
786
  fieldSlug: params.fieldSlug,
663
787
  });
664
788
  break;
789
+ case 'invite_client':
790
+ result = await (0, portal_clients_1.inviteClient)({
791
+ projectId: params.projectId,
792
+ email: params.email,
793
+ name: params.name,
794
+ permissions: params.permissions,
795
+ });
796
+ break;
797
+ case 'list_clients':
798
+ result = await (0, portal_clients_1.listClients)({
799
+ projectId: params.projectId,
800
+ });
801
+ break;
802
+ case 'list_invitations':
803
+ result = await (0, portal_clients_1.listInvitations)({
804
+ projectId: params.projectId,
805
+ });
806
+ break;
807
+ case 'update_client_permissions':
808
+ result = await (0, portal_clients_1.updateClientPermissions)({
809
+ projectId: params.projectId,
810
+ accessId: params.accessId,
811
+ permissions: params.permissions,
812
+ });
813
+ break;
814
+ case 'revoke_client_access':
815
+ result = await (0, portal_clients_1.revokeClientAccess)({
816
+ projectId: params.projectId,
817
+ accessId: params.accessId,
818
+ confirmRevoke: params.confirmRevoke,
819
+ });
820
+ break;
821
+ case 'cancel_invitation':
822
+ result = await (0, portal_clients_1.cancelInvitation)({
823
+ projectId: params.projectId,
824
+ invitationId: params.invitationId,
825
+ confirmCancel: params.confirmCancel,
826
+ });
827
+ break;
665
828
  default:
666
829
  return {
667
830
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -35,7 +35,7 @@ export declare function getAuthRequiredMessage(): string;
35
35
  */
36
36
  export interface ApiRequestOptions {
37
37
  tenantId?: string;
38
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
38
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
39
39
  body?: unknown;
40
40
  }
41
41
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/lib/api-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAgB5D;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAKxC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAW5D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAS/C;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG,QAAQ,CAAC,CAoEjC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,QAAQ,CAOlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAYD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAwCnH;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAM,GAChD,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG,QAAQ,CAAC,CAOjC"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/lib/api-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAgB5D;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAKxC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAW5D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAS/C;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG,QAAQ,CAAC,CAoEjC;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,QAAQ,CAOlE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAYD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAwCnH;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EACxC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAM,GAChD,OAAO,CAAC;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,GAAG,QAAQ,CAAC,CAOjC"}
@@ -6,7 +6,7 @@
6
6
  * 2. Intent Inference - Determine what user wants to do
7
7
  * 3. Guided Execution - Provide exact tool calls with real values
8
8
  */
9
- export type Intent = 'explore' | 'add_content' | 'update_schema' | 'convert' | 'deploy';
9
+ export type Intent = 'explore' | 'add_content' | 'update_schema' | 'convert' | 'deploy' | 'manage_clients';
10
10
  export interface GetStartedInput {
11
11
  intent?: Intent;
12
12
  projectId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"get-started.d.ts","sourceRoot":"","sources":["../../src/tools/get-started.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA8CH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,eAAe,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExF,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgnBD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CA4FxE"}
1
+ {"version":3,"file":"get-started.d.ts","sourceRoot":"","sources":["../../src/tools/get-started.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA8CH,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,gBAAgB,CAAC;AAE3G,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2rBD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHxE"}
@@ -101,6 +101,11 @@ get_started(intent: "deploy", projectId: "${firstProject.id}")
101
101
  get_started(intent: "convert")
102
102
  \`\`\`
103
103
 
104
+ ### Manage portal clients
105
+ \`\`\`
106
+ get_started(intent: "manage_clients", projectId: "${firstProject.id}")
107
+ \`\`\`
108
+
104
109
  ## Available Intents
105
110
 
106
111
  | Intent | Description |
@@ -110,6 +115,7 @@ get_started(intent: "convert")
110
115
  | \`update_schema\` | Add collections or fields |
111
116
  | \`convert\` | Build a new website from scratch |
112
117
  | \`deploy\` | Push changes to an existing site |
118
+ | \`manage_clients\` | Invite/manage client portal users |
113
119
  `;
114
120
  return output;
115
121
  }
@@ -578,6 +584,74 @@ deploy_package(
578
584
  `;
579
585
  return output;
580
586
  }
587
+ /**
588
+ * Build the manage_clients response
589
+ */
590
+ function buildManageClientsResponse(project) {
591
+ return `# Fast Mode MCP - Manage Portal Clients
592
+
593
+ ## Project: ${project.name}
594
+ **URL:** https://${project.subdomain}.fastmode.ai
595
+
596
+ ---
597
+
598
+ ## Workflow: Client Portal Management
599
+
600
+ ### Step 1: See who has access
601
+ \`\`\`
602
+ list_clients(projectId: "${project.id}")
603
+ \`\`\`
604
+
605
+ ### Step 2: See pending invitations
606
+ \`\`\`
607
+ list_invitations(projectId: "${project.id}")
608
+ \`\`\`
609
+
610
+ ### Step 3: Invite a new client
611
+ \`\`\`
612
+ invite_client(
613
+ projectId: "${project.id}",
614
+ email: "client@example.com",
615
+ name: "Client Name"
616
+ )
617
+ \`\`\`
618
+
619
+ ### Step 4: Update permissions
620
+ \`\`\`
621
+ update_client_permissions(
622
+ projectId: "${project.id}",
623
+ accessId: "ACCESS_ID_FROM_LIST",
624
+ permissions: ["cms.read", "cms.write", "editor"]
625
+ )
626
+ \`\`\`
627
+
628
+ ### Step 5: Revoke access (requires user confirmation)
629
+ \`\`\`
630
+ revoke_client_access(
631
+ projectId: "${project.id}",
632
+ accessId: "ACCESS_ID",
633
+ confirmRevoke: true
634
+ )
635
+ \`\`\`
636
+
637
+ ---
638
+
639
+ ## Available Permissions
640
+
641
+ | Permission | Description |
642
+ |------------|-------------|
643
+ | \`cms.read\` | View collection items |
644
+ | \`cms.write\` | Create/edit/archive/delete items |
645
+ | \`editor\` | Access visual editor |
646
+ | \`forms.read\` | View form submissions |
647
+ | \`dns\` | Manage DNS settings |
648
+ | \`api\` | Access API/integrations |
649
+ | \`notifications\` | Manage notification rules |
650
+ | \`billing\` | View plans and manage billing |
651
+
652
+ **Default permissions:** cms.read, cms.write, editor, forms.read
653
+ `;
654
+ }
581
655
  /**
582
656
  * Build unauthenticated response
583
657
  */
@@ -689,6 +763,25 @@ Could not find project: "${projectId}"
689
763
  Use \`list_projects\` to see available projects.`;
690
764
  }
691
765
  return buildDeployResponse(context.selectedProject);
766
+ case 'manage_clients':
767
+ if (!context.selectedProject) {
768
+ if (!projectId) {
769
+ return `# Project Required
770
+
771
+ To manage clients, specify which project:
772
+ \`\`\`
773
+ get_started(intent: "manage_clients", projectId: "your-project-id")
774
+ \`\`\`
775
+
776
+ ${buildExploreResponse(context)}`;
777
+ }
778
+ return `# Project Not Found
779
+
780
+ Could not find project: "${projectId}"
781
+
782
+ Use \`list_projects\` to see available projects.`;
783
+ }
784
+ return buildManageClientsResponse(context.selectedProject);
692
785
  case 'explore':
693
786
  default:
694
787
  // If projectId provided, show detailed project info
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Portal Client Management Tools
3
+ *
4
+ * Tools for inviting clients, managing permissions, and revoking access
5
+ * to the client portal via the portal-admin API.
6
+ */
7
+ /**
8
+ * Invite a client to the project portal
9
+ */
10
+ export declare function inviteClient(params: {
11
+ projectId: string;
12
+ email: string;
13
+ name?: string;
14
+ permissions?: string[];
15
+ }): Promise<string>;
16
+ /**
17
+ * List all portal clients with access to this project
18
+ */
19
+ export declare function listClients(params: {
20
+ projectId: string;
21
+ }): Promise<string>;
22
+ /**
23
+ * List pending portal invitations
24
+ */
25
+ export declare function listInvitations(params: {
26
+ projectId: string;
27
+ }): Promise<string>;
28
+ /**
29
+ * Update a portal client's permissions
30
+ */
31
+ export declare function updateClientPermissions(params: {
32
+ projectId: string;
33
+ accessId: string;
34
+ permissions: string[];
35
+ }): Promise<string>;
36
+ /**
37
+ * Revoke a client's portal access
38
+ * REQUIRES confirmRevoke: true to execute
39
+ */
40
+ export declare function revokeClientAccess(params: {
41
+ projectId: string;
42
+ accessId: string;
43
+ confirmRevoke: boolean;
44
+ }): Promise<string>;
45
+ /**
46
+ * Cancel a pending portal invitation
47
+ * REQUIRES confirmCancel: true to execute
48
+ */
49
+ export declare function cancelInvitation(params: {
50
+ projectId: string;
51
+ invitationId: string;
52
+ confirmCancel: boolean;
53
+ }): Promise<string>;
54
+ //# sourceMappingURL=portal-clients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-clients.d.ts","sourceRoot":"","sources":["../../src/tools/portal-clients.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiHH;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDlB;AAuBD;;GAEG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE;IACxC,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB;AAmDD;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB;AA4CD;;GAEG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0DlB;AAkBD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsDlB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsDlB"}
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ /**
3
+ * Portal Client Management Tools
4
+ *
5
+ * Tools for inviting clients, managing permissions, and revoking access
6
+ * to the client portal via the portal-admin API.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.inviteClient = inviteClient;
10
+ exports.listClients = listClients;
11
+ exports.listInvitations = listInvitations;
12
+ exports.updateClientPermissions = updateClientPermissions;
13
+ exports.revokeClientAccess = revokeClientAccess;
14
+ exports.cancelInvitation = cancelInvitation;
15
+ const api_client_1 = require("../lib/api-client");
16
+ const device_flow_1 = require("../lib/device-flow");
17
+ // ============ Helpers ============
18
+ const PERMISSION_DESCRIPTIONS = {
19
+ 'cms.read': 'View collection items',
20
+ 'cms.write': 'Create/edit/archive/delete items',
21
+ 'editor': 'Access visual editor',
22
+ 'forms.read': 'View form submissions',
23
+ 'dns': 'Manage DNS settings',
24
+ 'api': 'Access API/integrations',
25
+ 'notifications': 'Manage notification rules',
26
+ 'billing': 'View plans and manage billing',
27
+ };
28
+ const ALL_PERMISSIONS = Object.keys(PERMISSION_DESCRIPTIONS);
29
+ /**
30
+ * Type guard to check if prepare result failed
31
+ */
32
+ function prepFailed(prep) {
33
+ return !prep.success;
34
+ }
35
+ /**
36
+ * Helper to ensure authentication and resolve project ID
37
+ */
38
+ async function prepareRequest(projectId) {
39
+ if (await (0, api_client_1.needsAuthentication)()) {
40
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
41
+ if (!authResult.authenticated) {
42
+ return { success: false, message: authResult.message };
43
+ }
44
+ }
45
+ const resolved = await (0, api_client_1.resolveProjectId)(projectId);
46
+ if ('error' in resolved) {
47
+ return { success: false, message: `# Project Not Found\n\n${resolved.error}` };
48
+ }
49
+ return { success: true, tenantId: resolved.tenantId };
50
+ }
51
+ /**
52
+ * Format permissions as a readable list
53
+ */
54
+ function formatPermissions(permissions) {
55
+ return permissions
56
+ .map(p => `- \`${p}\` — ${PERMISSION_DESCRIPTIONS[p] || 'Unknown permission'}`)
57
+ .join('\n');
58
+ }
59
+ // ============ Exported Functions ============
60
+ /**
61
+ * Invite a client to the project portal
62
+ */
63
+ async function inviteClient(params) {
64
+ const prep = await prepareRequest(params.projectId);
65
+ if (prepFailed(prep))
66
+ return prep.message;
67
+ // Validate permissions if provided
68
+ if (params.permissions) {
69
+ const invalid = params.permissions.filter(p => !ALL_PERMISSIONS.includes(p));
70
+ if (invalid.length > 0) {
71
+ return `# Invalid Permissions\n\nUnknown permissions: ${invalid.map(p => `\`${p}\``).join(', ')}\n\n**Available permissions:**\n${ALL_PERMISSIONS.map(p => `- \`${p}\``).join('\n')}`;
72
+ }
73
+ }
74
+ const body = { email: params.email };
75
+ if (params.name)
76
+ body.name = params.name;
77
+ if (params.permissions)
78
+ body.permissions = params.permissions;
79
+ const response = await (0, api_client_1.apiRequest)('/api/portal-admin/invitations', {
80
+ tenantId: prep.tenantId,
81
+ method: 'POST',
82
+ body,
83
+ });
84
+ if ((0, api_client_1.isApiError)(response)) {
85
+ if ((0, api_client_1.needsAuthError)(response)) {
86
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
87
+ if (!authResult.authenticated)
88
+ return authResult.message;
89
+ const retry = await (0, api_client_1.apiRequest)('/api/portal-admin/invitations', {
90
+ tenantId: prep.tenantId,
91
+ method: 'POST',
92
+ body,
93
+ });
94
+ if ((0, api_client_1.isApiError)(retry)) {
95
+ return `# Error Inviting Client\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
96
+ }
97
+ return formatInviteResponse(retry.data);
98
+ }
99
+ return `# Error Inviting Client\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
100
+ }
101
+ return formatInviteResponse(response.data);
102
+ }
103
+ function formatInviteResponse(invite) {
104
+ const permissions = invite.permissions;
105
+ return `# Client Invited Successfully
106
+
107
+ **Email:** ${invite.email}${invite.name ? `\n**Name:** ${invite.name}` : ''}
108
+ **Expires:** ${new Date(invite.expiresAt).toLocaleDateString()}
109
+
110
+ ## Invite Link
111
+
112
+ > ${invite.inviteUrl}
113
+
114
+ **Share this link with the client.** They will create a password and get portal access. The link expires in 7 days.
115
+
116
+ ## Permissions Granted
117
+ ${formatPermissions(permissions)}
118
+
119
+ **Note:** The client portal has been auto-enabled for this project.
120
+ `;
121
+ }
122
+ /**
123
+ * List all portal clients with access to this project
124
+ */
125
+ async function listClients(params) {
126
+ const prep = await prepareRequest(params.projectId);
127
+ if (prepFailed(prep))
128
+ return prep.message;
129
+ const response = await (0, api_client_1.apiRequest)('/api/portal-admin/users', { tenantId: prep.tenantId, method: 'GET' });
130
+ if ((0, api_client_1.isApiError)(response)) {
131
+ if ((0, api_client_1.needsAuthError)(response)) {
132
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
133
+ if (!authResult.authenticated)
134
+ return authResult.message;
135
+ const retry = await (0, api_client_1.apiRequest)('/api/portal-admin/users', { tenantId: prep.tenantId, method: 'GET' });
136
+ if ((0, api_client_1.isApiError)(retry)) {
137
+ return `# Error Listing Clients\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
138
+ }
139
+ return formatClientsList(retry.data, params.projectId);
140
+ }
141
+ return `# Error Listing Clients\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
142
+ }
143
+ return formatClientsList(response.data, params.projectId);
144
+ }
145
+ function formatClientsList(clients, projectId) {
146
+ if (!clients || clients.length === 0) {
147
+ return `# No Portal Clients
148
+
149
+ No clients have access to this project's portal yet.
150
+
151
+ To invite a client:
152
+ \`\`\`
153
+ invite_client(projectId: "${projectId}", email: "client@example.com")
154
+ \`\`\`
155
+ `;
156
+ }
157
+ let output = `# Portal Clients
158
+
159
+ Found ${clients.length} client${clients.length !== 1 ? 's' : ''} with access:
160
+
161
+ | Name | Email | Permissions | Last Login | Access ID |
162
+ |------|-------|-------------|------------|-----------|
163
+ `;
164
+ for (const client of clients) {
165
+ const name = client.name || '-';
166
+ const perms = client.permissions.join(', ');
167
+ const lastLogin = client.lastLoginAt
168
+ ? new Date(client.lastLoginAt).toLocaleDateString()
169
+ : 'Never';
170
+ output += `| ${name} | ${client.email} | ${perms} | ${lastLogin} | \`${client.id}\` |\n`;
171
+ }
172
+ output += `
173
+ ---
174
+
175
+ ## Actions
176
+
177
+ **Update permissions:**
178
+ \`\`\`
179
+ update_client_permissions(projectId: "${projectId}", accessId: "ACCESS_ID", permissions: ["cms.read", "cms.write"])
180
+ \`\`\`
181
+
182
+ **Revoke access** (requires user confirmation):
183
+ \`\`\`
184
+ revoke_client_access(projectId: "${projectId}", accessId: "ACCESS_ID", confirmRevoke: true)
185
+ \`\`\`
186
+ `;
187
+ return output;
188
+ }
189
+ /**
190
+ * List pending portal invitations
191
+ */
192
+ async function listInvitations(params) {
193
+ const prep = await prepareRequest(params.projectId);
194
+ if (prepFailed(prep))
195
+ return prep.message;
196
+ const response = await (0, api_client_1.apiRequest)('/api/portal-admin/invitations', { tenantId: prep.tenantId, method: 'GET' });
197
+ if ((0, api_client_1.isApiError)(response)) {
198
+ if ((0, api_client_1.needsAuthError)(response)) {
199
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
200
+ if (!authResult.authenticated)
201
+ return authResult.message;
202
+ const retry = await (0, api_client_1.apiRequest)('/api/portal-admin/invitations', { tenantId: prep.tenantId, method: 'GET' });
203
+ if ((0, api_client_1.isApiError)(retry)) {
204
+ return `# Error Listing Invitations\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
205
+ }
206
+ return formatInvitationsList(retry.data, params.projectId);
207
+ }
208
+ return `# Error Listing Invitations\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
209
+ }
210
+ return formatInvitationsList(response.data, params.projectId);
211
+ }
212
+ function formatInvitationsList(invitations, projectId) {
213
+ if (!invitations || invitations.length === 0) {
214
+ return `# No Pending Invitations
215
+
216
+ There are no pending invitations for this project.
217
+
218
+ To invite a client:
219
+ \`\`\`
220
+ invite_client(projectId: "${projectId}", email: "client@example.com")
221
+ \`\`\`
222
+ `;
223
+ }
224
+ let output = `# Pending Invitations
225
+
226
+ Found ${invitations.length} pending invitation${invitations.length !== 1 ? 's' : ''}:
227
+
228
+ | Email | Name | Permissions | Expires | ID |
229
+ |-------|------|-------------|---------|----|
230
+ `;
231
+ for (const inv of invitations) {
232
+ const name = inv.name || '-';
233
+ const perms = inv.permissions.join(', ');
234
+ const expires = new Date(inv.expiresAt).toLocaleDateString();
235
+ output += `| ${inv.email} | ${name} | ${perms} | ${expires} | \`${inv.id}\` |\n`;
236
+ }
237
+ output += `
238
+ ---
239
+
240
+ ## Actions
241
+
242
+ **Cancel an invitation** (requires user confirmation):
243
+ \`\`\`
244
+ cancel_invitation(projectId: "${projectId}", invitationId: "INVITATION_ID", confirmCancel: true)
245
+ \`\`\`
246
+ `;
247
+ return output;
248
+ }
249
+ /**
250
+ * Update a portal client's permissions
251
+ */
252
+ async function updateClientPermissions(params) {
253
+ const prep = await prepareRequest(params.projectId);
254
+ if (prepFailed(prep))
255
+ return prep.message;
256
+ // Validate permissions
257
+ const invalid = params.permissions.filter(p => !ALL_PERMISSIONS.includes(p));
258
+ if (invalid.length > 0) {
259
+ return `# Invalid Permissions\n\nUnknown permissions: ${invalid.map(p => `\`${p}\``).join(', ')}\n\n**Available permissions:**\n${ALL_PERMISSIONS.map(p => `- \`${p}\``).join('\n')}`;
260
+ }
261
+ const response = await (0, api_client_1.apiRequest)(`/api/portal-admin/users/${params.accessId}`, {
262
+ tenantId: prep.tenantId,
263
+ method: 'PATCH',
264
+ body: { permissions: params.permissions },
265
+ });
266
+ if ((0, api_client_1.isApiError)(response)) {
267
+ if ((0, api_client_1.needsAuthError)(response)) {
268
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
269
+ if (!authResult.authenticated)
270
+ return authResult.message;
271
+ const retry = await (0, api_client_1.apiRequest)(`/api/portal-admin/users/${params.accessId}`, {
272
+ tenantId: prep.tenantId,
273
+ method: 'PATCH',
274
+ body: { permissions: params.permissions },
275
+ });
276
+ if ((0, api_client_1.isApiError)(retry)) {
277
+ return `# Error Updating Permissions\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
278
+ }
279
+ return formatPermissionsUpdate(retry.data);
280
+ }
281
+ return `# Error Updating Permissions\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
282
+ }
283
+ return formatPermissionsUpdate(response.data);
284
+ }
285
+ function formatPermissionsUpdate(client) {
286
+ return `# Permissions Updated
287
+
288
+ **Client:** ${client.email}${client.name ? ` (${client.name})` : ''}
289
+ **Access ID:** \`${client.id}\`
290
+
291
+ ## Updated Permissions
292
+ ${formatPermissions(client.permissions)}
293
+ `;
294
+ }
295
+ /**
296
+ * Revoke a client's portal access
297
+ * REQUIRES confirmRevoke: true to execute
298
+ */
299
+ async function revokeClientAccess(params) {
300
+ if (params.confirmRevoke !== true) {
301
+ return `# Confirmation Required
302
+
303
+ **You must get explicit permission from the user before revoking client access.**
304
+
305
+ To revoke access for client with access ID "${params.accessId}":
306
+
307
+ 1. Ask the user: "Are you sure you want to revoke this client's portal access?"
308
+ 2. Only after the user confirms, call this tool again with \`confirmRevoke: true\`
309
+
310
+ **Never revoke access without user confirmation.**
311
+ `;
312
+ }
313
+ const prep = await prepareRequest(params.projectId);
314
+ if (prepFailed(prep))
315
+ return prep.message;
316
+ const response = await (0, api_client_1.apiRequest)(`/api/portal-admin/users/${params.accessId}`, { tenantId: prep.tenantId, method: 'DELETE' });
317
+ if ((0, api_client_1.isApiError)(response)) {
318
+ if ((0, api_client_1.needsAuthError)(response)) {
319
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
320
+ if (!authResult.authenticated)
321
+ return authResult.message;
322
+ const retry = await (0, api_client_1.apiRequest)(`/api/portal-admin/users/${params.accessId}`, { tenantId: prep.tenantId, method: 'DELETE' });
323
+ if ((0, api_client_1.isApiError)(retry)) {
324
+ return `# Error Revoking Access\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
325
+ }
326
+ return `# Client Access Revoked
327
+
328
+ Successfully revoked portal access for access ID \`${params.accessId}\`.
329
+
330
+ The client can no longer access the portal for this project.
331
+ `;
332
+ }
333
+ return `# Error Revoking Access\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
334
+ }
335
+ return `# Client Access Revoked
336
+
337
+ Successfully revoked portal access for access ID \`${params.accessId}\`.
338
+
339
+ The client can no longer access the portal for this project.
340
+ `;
341
+ }
342
+ /**
343
+ * Cancel a pending portal invitation
344
+ * REQUIRES confirmCancel: true to execute
345
+ */
346
+ async function cancelInvitation(params) {
347
+ if (params.confirmCancel !== true) {
348
+ return `# Confirmation Required
349
+
350
+ **You must get explicit permission from the user before canceling an invitation.**
351
+
352
+ To cancel invitation "${params.invitationId}":
353
+
354
+ 1. Ask the user: "Are you sure you want to cancel this invitation?"
355
+ 2. Only after the user confirms, call this tool again with \`confirmCancel: true\`
356
+
357
+ **Never cancel invitations without user confirmation.**
358
+ `;
359
+ }
360
+ const prep = await prepareRequest(params.projectId);
361
+ if (prepFailed(prep))
362
+ return prep.message;
363
+ const response = await (0, api_client_1.apiRequest)(`/api/portal-admin/invitations/${params.invitationId}`, { tenantId: prep.tenantId, method: 'DELETE' });
364
+ if ((0, api_client_1.isApiError)(response)) {
365
+ if ((0, api_client_1.needsAuthError)(response)) {
366
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
367
+ if (!authResult.authenticated)
368
+ return authResult.message;
369
+ const retry = await (0, api_client_1.apiRequest)(`/api/portal-admin/invitations/${params.invitationId}`, { tenantId: prep.tenantId, method: 'DELETE' });
370
+ if ((0, api_client_1.isApiError)(retry)) {
371
+ return `# Error Canceling Invitation\n\n${retry.error}\n\n**Status:** ${retry.statusCode}`;
372
+ }
373
+ return `# Invitation Canceled
374
+
375
+ Successfully canceled invitation \`${params.invitationId}\`.
376
+
377
+ The invitation link will no longer work.
378
+ `;
379
+ }
380
+ return `# Error Canceling Invitation\n\n${response.error}\n\n**Status:** ${response.statusCode}`;
381
+ }
382
+ return `# Invitation Canceled
383
+
384
+ Successfully canceled invitation \`${params.invitationId}\`.
385
+
386
+ The invitation link will no longer work.
387
+ `;
388
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastmode-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for FastMode CMS. Convert websites, validate packages, and deploy directly to FastMode. Includes authentication, project creation, schema sync, and one-click deployment.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {