@fruition/fcp-mcp-server 1.25.0 → 1.26.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.
Files changed (2) hide show
  1. package/dist/index.js +53 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -198,19 +198,19 @@ const TOOL_PERMISSIONS = {
198
198
  fcp_get_bulk_notify_history: 'viewer',
199
199
  };
200
200
  // Resolved role for the current API key. Cached only on successful lookup so
201
- // transient FCP outages don't pin us to 'none' for the rest of the session.
201
+ // transient FCP outages don't pin us to a failure for the rest of the session.
202
202
  let cachedUserRole;
203
203
  async function fetchUserRole() {
204
204
  if (cachedUserRole !== undefined)
205
- return cachedUserRole;
205
+ return { ok: true, role: cachedUserRole };
206
206
  // dev_bypass is the local-dev token; treat as super_admin to avoid
207
207
  // gating local development against role lookup.
208
208
  if (FCP_API_TOKEN === 'dev_bypass') {
209
209
  cachedUserRole = 'super_admin';
210
- return cachedUserRole;
210
+ return { ok: true, role: 'super_admin' };
211
211
  }
212
212
  if (!FCP_API_TOKEN) {
213
- return 'none';
213
+ return { ok: false, kind: 'no_token' };
214
214
  }
215
215
  try {
216
216
  const res = await fetch(`${FCP_API_URL}/api/mcp/me`, {
@@ -219,17 +219,20 @@ async function fetchUserRole() {
219
219
  });
220
220
  if (!res.ok) {
221
221
  console.error(`[MCP Server] Role lookup failed: HTTP ${res.status}`);
222
- return 'none';
222
+ // 401/403 => the API key itself is bad (invalid/expired/revoked).
223
+ // Everything else (5xx, 429, ...) is treated as a transient failure.
224
+ const kind = res.status === 401 || res.status === 403 ? 'auth' : 'transient';
225
+ return { ok: false, kind, status: res.status };
223
226
  }
224
227
  const data = await res.json();
225
228
  const role = data?.role ?? 'none';
226
229
  cachedUserRole = role;
227
230
  console.error(`[MCP Server] Resolved user role: ${role} (${data?.email ?? 'unknown'})`);
228
- return role;
231
+ return { ok: true, role };
229
232
  }
230
233
  catch (err) {
231
234
  console.error('[MCP Server] Role lookup error:', err);
232
- return 'none';
235
+ return { ok: false, kind: 'transient' };
233
236
  }
234
237
  }
235
238
  function isRoleAtLeast(actual, required) {
@@ -243,7 +246,26 @@ function isRoleAtLeast(actual, required) {
243
246
  }
244
247
  async function enforceToolPermission(toolName) {
245
248
  const required = TOOL_PERMISSIONS[toolName] ?? 'viewer';
246
- const actual = await fetchUserRole();
249
+ const lookup = await fetchUserRole();
250
+ // A failed lookup is NOT a role problem. Surface the real cause so the user
251
+ // takes the right action instead of asking a super_admin for a role they
252
+ // may already effectively have.
253
+ if (!lookup.ok) {
254
+ switch (lookup.kind) {
255
+ case 'no_token':
256
+ throw new Error('FCP_API_TOKEN is not set. Add your FCP API key to this MCP server\'s ' +
257
+ 'env config (create one in FCP → Admin → API Keys).');
258
+ case 'auth':
259
+ throw new Error(`Your FCP API key is invalid, expired, or revoked (HTTP ${lookup.status}). ` +
260
+ 'Create a new key in FCP → Admin → API Keys and update your MCP server ' +
261
+ 'config. This is NOT a role/permission problem — do not request a role grant.');
262
+ case 'transient':
263
+ throw new Error('FCP role lookup is temporarily unavailable' +
264
+ (lookup.status ? ` (HTTP ${lookup.status})` : '') +
265
+ '. This is a transient FCP error — retry shortly.');
266
+ }
267
+ }
268
+ const actual = lookup.role;
247
269
  if (!isRoleAtLeast(actual, required)) {
248
270
  throw new Error(`Permission denied: tool '${toolName}' requires ${required} role; ` +
249
271
  `your role is '${actual}'. Contact a super_admin (Brad, Mattox, or Andrea) ` +
@@ -2811,7 +2833,7 @@ const TOOLS = [
2811
2833
  },
2812
2834
  {
2813
2835
  name: 'fcp_shield_add_domain',
2814
- description: 'Add a domain to Fruition Shield WAF protection. Returns DNS records (traffic CNAME + cert validation CNAME) that must be added at the registrar.',
2836
+ description: 'Add a domain to Fruition Shield WAF protection. Custom mode (default) fronts an origin server via the shared distribution. S3 mode (origin_type="s3") provisions a DEDICATED CloudFront distribution + OAC over a private S3 bucket (Steldris static hosting) and returns its distribution id + CloudFront domain. Returns DNS records to add at the registrar.',
2815
2837
  inputSchema: {
2816
2838
  type: 'object',
2817
2839
  properties: {
@@ -2819,9 +2841,14 @@ const TOOLS = [
2819
2841
  type: 'string',
2820
2842
  description: 'Domain to protect (e.g., www.butterflies.org)',
2821
2843
  },
2844
+ origin_type: {
2845
+ type: 'string',
2846
+ enum: ['custom', 's3'],
2847
+ description: 'Origin mode: custom (shared distribution + origin server, default) or s3 (dedicated distribution + OAC over a private S3 bucket).',
2848
+ },
2822
2849
  origin_host: {
2823
2850
  type: 'string',
2824
- description: 'Origin server IP or hostname (e.g., 146.190.2.169 for K8s prod)',
2851
+ description: 'Origin server IP or hostname (e.g., 146.190.2.169 for K8s prod). Required for origin_type=custom; ignored for s3.',
2825
2852
  },
2826
2853
  origin_port: {
2827
2854
  type: 'number',
@@ -2831,6 +2858,18 @@ const TOOLS = [
2831
2858
  type: 'string',
2832
2859
  description: 'Origin protocol: http or https (default: https)',
2833
2860
  },
2861
+ s3_bucket: {
2862
+ type: 'string',
2863
+ description: 'S3 artifacts bucket (origin_type=s3, e.g. steldris-sites-prod). Required for s3 mode.',
2864
+ },
2865
+ s3_region: {
2866
+ type: 'string',
2867
+ description: 'S3 bucket region (origin_type=s3, e.g. us-east-1). Required for s3 mode.',
2868
+ },
2869
+ s3_origin_path: {
2870
+ type: 'string',
2871
+ description: 'CloudFront OriginPath — the per-tenant key prefix (origin_type=s3, e.g. /acme).',
2872
+ },
2834
2873
  website_id: {
2835
2874
  type: 'number',
2836
2875
  description: 'Link to existing FCP website ID',
@@ -2841,14 +2880,14 @@ const TOOLS = [
2841
2880
  },
2842
2881
  cache_profile: {
2843
2882
  type: 'string',
2844
- description: 'Cache profile: standard, aggressive, minimal, none (default: standard)',
2883
+ description: 'Cache profile: standard, aggressive, minimal, none (default: standard; s3 defaults to aggressive)',
2845
2884
  },
2846
2885
  notes: {
2847
2886
  type: 'string',
2848
2887
  description: 'Notes about this domain',
2849
2888
  },
2850
2889
  },
2851
- required: ['domain', 'origin_host'],
2890
+ required: ['domain'],
2852
2891
  },
2853
2892
  },
2854
2893
  {
@@ -4734,9 +4773,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4734
4773
  };
4735
4774
  }
4736
4775
  case 'fcp_shield_add_domain': {
4737
- const { domain, origin_host, origin_port, origin_protocol, website_id, account_id, cache_profile, notes } = args;
4776
+ const { domain, origin_type, origin_host, origin_port, origin_protocol, s3_bucket, s3_region, s3_origin_path, website_id, account_id, cache_profile, notes } = args;
4738
4777
  const result = await client.shieldAddDomain({
4739
- domain, origin_host, origin_port, origin_protocol, website_id, account_id, cache_profile, notes,
4778
+ domain, origin_type, origin_host, origin_port, origin_protocol, s3_bucket, s3_region, s3_origin_path, website_id, account_id, cache_profile, notes,
4740
4779
  });
4741
4780
  return {
4742
4781
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruition/fcp-mcp-server",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "MCP Server for FCP Launch Coordination System - enables Claude Code to interact with FCP launches and track development time",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",