@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.
- package/README.md +3 -0
- package/package.json +1 -1
- 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
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("<", "<")
|
|
305
|
+
.replaceAll(">", ">")
|
|
306
|
+
.replaceAll('"', """)
|
|
307
|
+
.replaceAll("'", "'");
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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: {
|
|
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
|
-
"
|
|
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 =
|
|
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);
|