@flomenco/claude-plugin-mcp 0.1.1 → 0.1.3

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 (3) hide show
  1. package/README.md +3 -0
  2. package/package.json +1 -1
  3. package/src/index.js +243 -10
package/README.md CHANGED
@@ -11,6 +11,7 @@ It exposes authentication + plugin tools:
11
11
  - `flo_auth_logout`
12
12
 
13
13
  - `flo_search`
14
+ - `flo_query`
14
15
  - `flo_skill_routing`
15
16
  - `flo_qc_logo`
16
17
  - `flo_command` (raw passthrough for troubleshooting)
@@ -161,6 +162,7 @@ After Claude starts with MCP enabled:
161
162
  - Run `flo_auth_setup_help` if you need the Flo settings URL for locating `FLO_OAUTH_CLIENT_ID`
162
163
  - Run `flo_plugin_healthcheck` (expect `runtime.reachable: true`)
163
164
  - Run `flo_search` with `query="hail mary"`
165
+ - Or run `flo_query` with `query="hail mary"` to get filename-first results and next command templates
164
166
  - `flo_skill_routing` with an `assetId` from search
165
167
  - `flo_qc_logo` with `assetId` and `referenceAssetId`
166
168
 
@@ -171,5 +173,6 @@ One-shot E2E:
171
173
  Expected output shape:
172
174
 
173
175
  - search: `status`, `menuItems[]`
176
+ - query: `status`, `filenames[]`, `results[]`
174
177
  - skill-routing: `status`, `skills[]`, includes `/flo:qc-logo`
175
178
  - qc-logo: `status`, `result.overall|summary|assetId|assetUrl|findings`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flomenco/claude-plugin-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Flo Claude Co-Work plugin bridge for Claude Desktop/Code",
5
5
  "private": false,
6
6
  "license": "UNLICENSED",
package/src/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
4
  import {
@@ -18,6 +17,7 @@ import { spawn } from "node:child_process";
18
17
  const DEFAULT_TIMEOUT_MS = 60_000;
19
18
  const OAUTH_WAIT_TIMEOUT_MS = 180_000;
20
19
  const TOKEN_REFRESH_SKEW_SECONDS = 60;
20
+ const ASSET_ID_REGEX = /^[A-Za-z0-9._-]+$/;
21
21
  const DEFAULT_TOKEN_CACHE_PATH = path.join(
22
22
  os.homedir(),
23
23
  ".flo",
@@ -79,6 +79,17 @@ function defaultSettingsUrlForEnv() {
79
79
  return "https://dev.floapp.co/settings/api";
80
80
  }
81
81
 
82
+ function defaultWebAppUrlForEnv() {
83
+ const env = normalizedPluginEnv();
84
+ if (env === "prod") {
85
+ return "https://floapp.co";
86
+ }
87
+ if (env === "stg") {
88
+ return "https://stg.floapp.co";
89
+ }
90
+ return "https://dev.floapp.co";
91
+ }
92
+
82
93
  function getInvocationUrl() {
83
94
  const raw =
84
95
  process.env.FLO_INTERFACE_AGENT_INVOCATION_URL ||
@@ -120,6 +131,8 @@ function getOAuthConfig(strict = false) {
120
131
  process.env.FLO_OAUTH_CLIENT_ID_HELP_URL ||
121
132
  defaultSettingsUrlForEnv()
122
133
  ).trim();
134
+ const rawAppLoginUrl = (process.env.FLO_OAUTH_APP_LOGIN_URL || "").trim();
135
+ const appLoginUrl = rawAppLoginUrl || `${defaultWebAppUrlForEnv()}/login`;
123
136
 
124
137
  const ready = Boolean(authorizeUrl && tokenUrl && clientId);
125
138
  if (strict && !ready) {
@@ -142,6 +155,7 @@ function getOAuthConfig(strict = false) {
142
155
  userPoolName,
143
156
  expectedClientName,
144
157
  settingsUrl,
158
+ appLoginUrl,
145
159
  };
146
160
  }
147
161
 
@@ -284,6 +298,127 @@ function openBrowser(url) {
284
298
  }
285
299
  }
286
300
 
301
+ function escapeHtml(value) {
302
+ return String(value || "")
303
+ .replaceAll("&", "&")
304
+ .replaceAll("<", "&lt;")
305
+ .replaceAll(">", "&gt;")
306
+ .replaceAll('"', "&quot;")
307
+ .replaceAll("'", "&#39;");
308
+ }
309
+
310
+ function renderOAuthCallbackPage({ status, title, message, details }) {
311
+ const isSuccess = status === "success";
312
+ const badgeText = isSuccess ? "Flo Connected" : "Flo Connection Error";
313
+ const badgeColor = isSuccess ? "#22c55e" : "#ef4444";
314
+ const symbol = isSuccess ? "✓" : "!";
315
+ const detailsHtml = details
316
+ ? `<pre class="details">${escapeHtml(details)}</pre>`
317
+ : "";
318
+
319
+ return `<!doctype html>
320
+ <html lang="en">
321
+ <head>
322
+ <meta charset="utf-8" />
323
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
324
+ <title>${escapeHtml(title)}</title>
325
+ <style>
326
+ :root {
327
+ color-scheme: dark;
328
+ }
329
+ body {
330
+ margin: 0;
331
+ min-height: 100vh;
332
+ font-family:
333
+ Inter,
334
+ -apple-system,
335
+ BlinkMacSystemFont,
336
+ "Segoe UI",
337
+ Roboto,
338
+ "Helvetica Neue",
339
+ Arial,
340
+ sans-serif;
341
+ background: radial-gradient(circle at top, #1a2138 0%, #0b1020 58%);
342
+ color: #e5e7eb;
343
+ display: grid;
344
+ place-items: center;
345
+ padding: 24px;
346
+ }
347
+ .card {
348
+ width: min(560px, 100%);
349
+ border: 1px solid rgba(148, 163, 184, 0.28);
350
+ background: rgba(15, 23, 42, 0.76);
351
+ backdrop-filter: blur(8px);
352
+ border-radius: 16px;
353
+ padding: 26px;
354
+ box-shadow: 0 16px 42px rgba(2, 6, 23, 0.35);
355
+ }
356
+ .badge {
357
+ display: inline-flex;
358
+ align-items: center;
359
+ gap: 8px;
360
+ border-radius: 999px;
361
+ border: 1px solid ${badgeColor};
362
+ color: ${badgeColor};
363
+ padding: 6px 10px;
364
+ font-size: 12px;
365
+ font-weight: 700;
366
+ letter-spacing: 0.02em;
367
+ }
368
+ .badge span {
369
+ display: inline-grid;
370
+ place-items: center;
371
+ width: 16px;
372
+ height: 16px;
373
+ border-radius: 999px;
374
+ background: ${badgeColor};
375
+ color: #ffffff;
376
+ font-size: 12px;
377
+ line-height: 1;
378
+ }
379
+ h1 {
380
+ margin: 14px 0 10px;
381
+ font-size: 24px;
382
+ line-height: 1.2;
383
+ }
384
+ p {
385
+ margin: 0;
386
+ color: #cbd5e1;
387
+ line-height: 1.5;
388
+ }
389
+ .hint {
390
+ margin-top: 16px;
391
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
392
+ padding-top: 14px;
393
+ font-size: 14px;
394
+ color: #94a3b8;
395
+ }
396
+ .details {
397
+ margin: 14px 0 0;
398
+ padding: 12px;
399
+ border-radius: 10px;
400
+ border: 1px solid rgba(148, 163, 184, 0.26);
401
+ background: rgba(2, 6, 23, 0.55);
402
+ color: #dbeafe;
403
+ white-space: pre-wrap;
404
+ word-break: break-word;
405
+ font-size: 12px;
406
+ line-height: 1.45;
407
+ }
408
+ </style>
409
+ </head>
410
+ <body>
411
+ <main class="card">
412
+ <div class="badge"><span>${symbol}</span>${badgeText}</div>
413
+ <h1>${escapeHtml(title)}</h1>
414
+ <p>${escapeHtml(message)}</p>
415
+ ${detailsHtml}
416
+ <p class="hint">You can close this tab and return to Claude.</p>
417
+ </main>
418
+ </body>
419
+ </html>`;
420
+ }
421
+
287
422
  async function waitForOAuthCode(redirectUri, expectedState) {
288
423
  const parsed = new URL(redirectUri);
289
424
  if (parsed.protocol !== "http:") {
@@ -314,21 +449,43 @@ async function waitForOAuthCode(redirectUri, expectedState) {
314
449
  if (oauthError) {
315
450
  clearTimeout(timer);
316
451
  res.statusCode = 400;
317
- res.end("OAuth failed. You can close this window.");
452
+ res.setHeader("content-type", "text/html; charset=utf-8");
453
+ res.end(
454
+ renderOAuthCallbackPage({
455
+ status: "error",
456
+ title: "Flo OAuth failed",
457
+ message: "The identity provider returned an error.",
458
+ details: `error=${oauthError}`,
459
+ })
460
+ );
318
461
  reject(new Error(`OAuth provider returned error=${oauthError}`));
319
462
  return;
320
463
  }
321
464
  if (!returnedCode) {
322
465
  clearTimeout(timer);
323
466
  res.statusCode = 400;
324
- res.end("Missing code. You can close this window.");
467
+ res.setHeader("content-type", "text/html; charset=utf-8");
468
+ res.end(
469
+ renderOAuthCallbackPage({
470
+ status: "error",
471
+ title: "Flo OAuth failed",
472
+ message: "The callback did not include an authorization code.",
473
+ })
474
+ );
325
475
  reject(new Error("OAuth callback missing code"));
326
476
  return;
327
477
  }
328
478
  if (returnedState !== expectedState) {
329
479
  clearTimeout(timer);
330
480
  res.statusCode = 400;
331
- res.end("Invalid state. You can close this window.");
481
+ res.setHeader("content-type", "text/html; charset=utf-8");
482
+ res.end(
483
+ renderOAuthCallbackPage({
484
+ status: "error",
485
+ title: "Flo OAuth failed",
486
+ message: "State mismatch detected. Please retry login.",
487
+ })
488
+ );
332
489
  reject(new Error("OAuth state mismatch"));
333
490
  return;
334
491
  }
@@ -337,7 +494,12 @@ async function waitForOAuthCode(redirectUri, expectedState) {
337
494
  res.statusCode = 200;
338
495
  res.setHeader("content-type", "text/html; charset=utf-8");
339
496
  res.end(
340
- "<html><body><h3>Flo OAuth complete.</h3><p>You can close this tab and return to Claude.</p></body></html>"
497
+ renderOAuthCallbackPage({
498
+ status: "success",
499
+ title: "Flo OAuth complete",
500
+ message:
501
+ "Authentication succeeded and your plugin token is now linked.",
502
+ })
341
503
  );
342
504
  resolve(returnedCode);
343
505
  } catch (err) {
@@ -496,6 +658,17 @@ function requireString(value, fieldName) {
496
658
  return value.trim();
497
659
  }
498
660
 
661
+ function requireAssetId(value, fieldName) {
662
+ const assetId = requireString(value, fieldName);
663
+ if (assetId.length > 128 || !ASSET_ID_REGEX.test(assetId)) {
664
+ throw new McpError(
665
+ ErrorCode.InvalidParams,
666
+ `\`${fieldName}\` must match ^[A-Za-z0-9._-]+$ and be <= 128 chars.`
667
+ );
668
+ }
669
+ return assetId;
670
+ }
671
+
499
672
  function assertSearchPayload(payload) {
500
673
  if (!payload || payload.status !== "ok" || !Array.isArray(payload.menuItems)) {
501
674
  throw new McpError(
@@ -595,7 +768,7 @@ const tools = [
595
768
  {
596
769
  name: "flo_auth_login",
597
770
  description:
598
- "Start OAuth login (browser PKCE flow), cache token locally, and return auth status.",
771
+ "Start OAuth login (browser PKCE flow), cache token locally, and return auth status. If prompted by hosted login UI, first sign in via appLoginUrl.",
599
772
  inputSchema: {
600
773
  type: "object",
601
774
  properties: {},
@@ -630,6 +803,28 @@ const tools = [
630
803
  additionalProperties: false,
631
804
  },
632
805
  },
806
+ {
807
+ name: "flo_analyze",
808
+ description:
809
+ "Analyze an asset directly (friendly alias for /flo:analyze-image <assetId>).",
810
+ inputSchema: {
811
+ type: "object",
812
+ properties: {
813
+ assetId: {
814
+ type: "string",
815
+ minLength: 1,
816
+ maxLength: 128,
817
+ pattern: "^[A-Za-z0-9._-]+$",
818
+ },
819
+ authToken: {
820
+ type: "string",
821
+ description: "Optional bearer token override for this call only.",
822
+ },
823
+ },
824
+ required: ["assetId"],
825
+ additionalProperties: false,
826
+ },
827
+ },
633
828
  {
634
829
  name: "flo_search",
635
830
  description:
@@ -647,14 +842,36 @@ const tools = [
647
842
  additionalProperties: false,
648
843
  },
649
844
  },
845
+ {
846
+ name: "flo_query",
847
+ description:
848
+ "Run /flo:query against interface-agent and return filename-first candidates plus follow-up commands.",
849
+ inputSchema: {
850
+ type: "object",
851
+ properties: {
852
+ query: { type: "string", minLength: 1 },
853
+ authToken: {
854
+ type: "string",
855
+ description: "Optional bearer token override for this call only.",
856
+ },
857
+ },
858
+ required: ["query"],
859
+ additionalProperties: false,
860
+ },
861
+ },
650
862
  {
651
863
  name: "flo_skill_routing",
652
864
  description:
653
- "Run /flo:skill-routing for an asset and return available plugin skills.",
865
+ "List available actions for an asset (what can I do with this file?).",
654
866
  inputSchema: {
655
867
  type: "object",
656
868
  properties: {
657
- assetId: { type: "string", minLength: 1 },
869
+ assetId: {
870
+ type: "string",
871
+ minLength: 1,
872
+ maxLength: 128,
873
+ pattern: "^[A-Za-z0-9._-]+$",
874
+ },
658
875
  authToken: {
659
876
  type: "string",
660
877
  description: "Optional bearer token override for this call only.",
@@ -796,6 +1013,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
796
1013
  oauthUserPoolName: oauth.userPoolName,
797
1014
  oauthUserPoolId: oauth.userPoolId || null,
798
1015
  oauthSettingsUrl: oauth.settingsUrl,
1016
+ appLoginUrl: oauth.appLoginUrl,
799
1017
  envTokenConfigured: envToken,
800
1018
  cachePath: getTokenCachePath(),
801
1019
  cachedTokenPresent: Boolean(cachedToken?.access_token),
@@ -818,11 +1036,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
818
1036
  oauthUserPoolName: oauth.userPoolName,
819
1037
  oauthUserPoolId: oauth.userPoolId || null,
820
1038
  oauthSettingsUrl: oauth.settingsUrl,
1039
+ appLoginUrl: oauth.appLoginUrl,
821
1040
  message:
822
- "Open oauthSettingsUrl, then copy the app client id for oauthExpectedClientName into FLO_OAUTH_CLIENT_ID.",
1041
+ "If hosted login UI appears, sign in at appLoginUrl first, then run flo_auth_login again. Open oauthSettingsUrl to copy client id for oauthExpectedClientName into FLO_OAUTH_CLIENT_ID.",
823
1042
  });
824
1043
  }
825
1044
 
1045
+ if (name === "flo_analyze") {
1046
+ const assetId = requireAssetId(args.assetId, "assetId");
1047
+ const prompt = `/flo:analyze-image ${assetId}`;
1048
+ const bodyText = await invokeInterfaceAgent(prompt, args.authToken);
1049
+ return asTextResult(parseMaybeJson(bodyText) || bodyText);
1050
+ }
1051
+
826
1052
  if (name === "flo_auth_logout") {
827
1053
  await clearCachedToken();
828
1054
  return asTextResult({
@@ -839,8 +1065,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
839
1065
  return asTextResult(parseMaybeJson(bodyText) || bodyText);
840
1066
  }
841
1067
 
1068
+ if (name === "flo_query") {
1069
+ const query = requireString(args.query, "query");
1070
+ const prompt = `/flo:query ${query}`;
1071
+ const bodyText = await invokeInterfaceAgent(prompt, args.authToken);
1072
+ return asTextResult(parseMaybeJson(bodyText) || bodyText);
1073
+ }
1074
+
842
1075
  if (name === "flo_skill_routing") {
843
- const assetId = requireString(args.assetId, "assetId");
1076
+ const assetId = requireAssetId(args.assetId, "assetId");
844
1077
  const prompt = `/flo:skill-routing ${assetId}`;
845
1078
  const bodyText = await invokeInterfaceAgent(prompt, args.authToken);
846
1079
  return asTextResult(parseMaybeJson(bodyText) || bodyText);