@flomenco/claude-plugin-mcp 0.1.0 → 0.1.2
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 +2 -1
- package/src/index.js +177 -4
- package/tools/install-claude-desktop.js +238 -0
- package/tools/print-bootstrap-config.js +186 -0
- package/tools/uninstall-claude-desktop.js +48 -0
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.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Flo Claude Co-Work plugin bridge for Claude Desktop/Code",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"type": "module",
|
|
23
23
|
"files": [
|
|
24
24
|
"src",
|
|
25
|
+
"tools",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
28
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import {
|
|
@@ -282,6 +283,127 @@ function openBrowser(url) {
|
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
function escapeHtml(value) {
|
|
287
|
+
return String(value || "")
|
|
288
|
+
.replaceAll("&", "&")
|
|
289
|
+
.replaceAll("<", "<")
|
|
290
|
+
.replaceAll(">", ">")
|
|
291
|
+
.replaceAll('"', """)
|
|
292
|
+
.replaceAll("'", "'");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderOAuthCallbackPage({ status, title, message, details }) {
|
|
296
|
+
const isSuccess = status === "success";
|
|
297
|
+
const badgeText = isSuccess ? "Flo Connected" : "Flo Connection Error";
|
|
298
|
+
const badgeColor = isSuccess ? "#22c55e" : "#ef4444";
|
|
299
|
+
const symbol = isSuccess ? "✓" : "!";
|
|
300
|
+
const detailsHtml = details
|
|
301
|
+
? `<pre class="details">${escapeHtml(details)}</pre>`
|
|
302
|
+
: "";
|
|
303
|
+
|
|
304
|
+
return `<!doctype html>
|
|
305
|
+
<html lang="en">
|
|
306
|
+
<head>
|
|
307
|
+
<meta charset="utf-8" />
|
|
308
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
309
|
+
<title>${escapeHtml(title)}</title>
|
|
310
|
+
<style>
|
|
311
|
+
:root {
|
|
312
|
+
color-scheme: dark;
|
|
313
|
+
}
|
|
314
|
+
body {
|
|
315
|
+
margin: 0;
|
|
316
|
+
min-height: 100vh;
|
|
317
|
+
font-family:
|
|
318
|
+
Inter,
|
|
319
|
+
-apple-system,
|
|
320
|
+
BlinkMacSystemFont,
|
|
321
|
+
"Segoe UI",
|
|
322
|
+
Roboto,
|
|
323
|
+
"Helvetica Neue",
|
|
324
|
+
Arial,
|
|
325
|
+
sans-serif;
|
|
326
|
+
background: radial-gradient(circle at top, #1a2138 0%, #0b1020 58%);
|
|
327
|
+
color: #e5e7eb;
|
|
328
|
+
display: grid;
|
|
329
|
+
place-items: center;
|
|
330
|
+
padding: 24px;
|
|
331
|
+
}
|
|
332
|
+
.card {
|
|
333
|
+
width: min(560px, 100%);
|
|
334
|
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
335
|
+
background: rgba(15, 23, 42, 0.76);
|
|
336
|
+
backdrop-filter: blur(8px);
|
|
337
|
+
border-radius: 16px;
|
|
338
|
+
padding: 26px;
|
|
339
|
+
box-shadow: 0 16px 42px rgba(2, 6, 23, 0.35);
|
|
340
|
+
}
|
|
341
|
+
.badge {
|
|
342
|
+
display: inline-flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
gap: 8px;
|
|
345
|
+
border-radius: 999px;
|
|
346
|
+
border: 1px solid ${badgeColor};
|
|
347
|
+
color: ${badgeColor};
|
|
348
|
+
padding: 6px 10px;
|
|
349
|
+
font-size: 12px;
|
|
350
|
+
font-weight: 700;
|
|
351
|
+
letter-spacing: 0.02em;
|
|
352
|
+
}
|
|
353
|
+
.badge span {
|
|
354
|
+
display: inline-grid;
|
|
355
|
+
place-items: center;
|
|
356
|
+
width: 16px;
|
|
357
|
+
height: 16px;
|
|
358
|
+
border-radius: 999px;
|
|
359
|
+
background: ${badgeColor};
|
|
360
|
+
color: #ffffff;
|
|
361
|
+
font-size: 12px;
|
|
362
|
+
line-height: 1;
|
|
363
|
+
}
|
|
364
|
+
h1 {
|
|
365
|
+
margin: 14px 0 10px;
|
|
366
|
+
font-size: 24px;
|
|
367
|
+
line-height: 1.2;
|
|
368
|
+
}
|
|
369
|
+
p {
|
|
370
|
+
margin: 0;
|
|
371
|
+
color: #cbd5e1;
|
|
372
|
+
line-height: 1.5;
|
|
373
|
+
}
|
|
374
|
+
.hint {
|
|
375
|
+
margin-top: 16px;
|
|
376
|
+
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
|
377
|
+
padding-top: 14px;
|
|
378
|
+
font-size: 14px;
|
|
379
|
+
color: #94a3b8;
|
|
380
|
+
}
|
|
381
|
+
.details {
|
|
382
|
+
margin: 14px 0 0;
|
|
383
|
+
padding: 12px;
|
|
384
|
+
border-radius: 10px;
|
|
385
|
+
border: 1px solid rgba(148, 163, 184, 0.26);
|
|
386
|
+
background: rgba(2, 6, 23, 0.55);
|
|
387
|
+
color: #dbeafe;
|
|
388
|
+
white-space: pre-wrap;
|
|
389
|
+
word-break: break-word;
|
|
390
|
+
font-size: 12px;
|
|
391
|
+
line-height: 1.45;
|
|
392
|
+
}
|
|
393
|
+
</style>
|
|
394
|
+
</head>
|
|
395
|
+
<body>
|
|
396
|
+
<main class="card">
|
|
397
|
+
<div class="badge"><span>${symbol}</span>${badgeText}</div>
|
|
398
|
+
<h1>${escapeHtml(title)}</h1>
|
|
399
|
+
<p>${escapeHtml(message)}</p>
|
|
400
|
+
${detailsHtml}
|
|
401
|
+
<p class="hint">You can close this tab and return to Claude.</p>
|
|
402
|
+
</main>
|
|
403
|
+
</body>
|
|
404
|
+
</html>`;
|
|
405
|
+
}
|
|
406
|
+
|
|
285
407
|
async function waitForOAuthCode(redirectUri, expectedState) {
|
|
286
408
|
const parsed = new URL(redirectUri);
|
|
287
409
|
if (parsed.protocol !== "http:") {
|
|
@@ -312,21 +434,43 @@ async function waitForOAuthCode(redirectUri, expectedState) {
|
|
|
312
434
|
if (oauthError) {
|
|
313
435
|
clearTimeout(timer);
|
|
314
436
|
res.statusCode = 400;
|
|
315
|
-
res.
|
|
437
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
438
|
+
res.end(
|
|
439
|
+
renderOAuthCallbackPage({
|
|
440
|
+
status: "error",
|
|
441
|
+
title: "Flo OAuth failed",
|
|
442
|
+
message: "The identity provider returned an error.",
|
|
443
|
+
details: `error=${oauthError}`,
|
|
444
|
+
})
|
|
445
|
+
);
|
|
316
446
|
reject(new Error(`OAuth provider returned error=${oauthError}`));
|
|
317
447
|
return;
|
|
318
448
|
}
|
|
319
449
|
if (!returnedCode) {
|
|
320
450
|
clearTimeout(timer);
|
|
321
451
|
res.statusCode = 400;
|
|
322
|
-
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 callback did not include an authorization code.",
|
|
458
|
+
})
|
|
459
|
+
);
|
|
323
460
|
reject(new Error("OAuth callback missing code"));
|
|
324
461
|
return;
|
|
325
462
|
}
|
|
326
463
|
if (returnedState !== expectedState) {
|
|
327
464
|
clearTimeout(timer);
|
|
328
465
|
res.statusCode = 400;
|
|
329
|
-
res.
|
|
466
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
467
|
+
res.end(
|
|
468
|
+
renderOAuthCallbackPage({
|
|
469
|
+
status: "error",
|
|
470
|
+
title: "Flo OAuth failed",
|
|
471
|
+
message: "State mismatch detected. Please retry login.",
|
|
472
|
+
})
|
|
473
|
+
);
|
|
330
474
|
reject(new Error("OAuth state mismatch"));
|
|
331
475
|
return;
|
|
332
476
|
}
|
|
@@ -335,7 +479,12 @@ async function waitForOAuthCode(redirectUri, expectedState) {
|
|
|
335
479
|
res.statusCode = 200;
|
|
336
480
|
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
337
481
|
res.end(
|
|
338
|
-
|
|
482
|
+
renderOAuthCallbackPage({
|
|
483
|
+
status: "success",
|
|
484
|
+
title: "Flo OAuth complete",
|
|
485
|
+
message:
|
|
486
|
+
"Authentication succeeded and your plugin token is now linked.",
|
|
487
|
+
})
|
|
339
488
|
);
|
|
340
489
|
resolve(returnedCode);
|
|
341
490
|
} catch (err) {
|
|
@@ -645,6 +794,23 @@ const tools = [
|
|
|
645
794
|
additionalProperties: false,
|
|
646
795
|
},
|
|
647
796
|
},
|
|
797
|
+
{
|
|
798
|
+
name: "flo_query",
|
|
799
|
+
description:
|
|
800
|
+
"Run /flo:query against interface-agent and return filename-first candidates plus follow-up commands.",
|
|
801
|
+
inputSchema: {
|
|
802
|
+
type: "object",
|
|
803
|
+
properties: {
|
|
804
|
+
query: { type: "string", minLength: 1 },
|
|
805
|
+
authToken: {
|
|
806
|
+
type: "string",
|
|
807
|
+
description: "Optional bearer token override for this call only.",
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
required: ["query"],
|
|
811
|
+
additionalProperties: false,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
648
814
|
{
|
|
649
815
|
name: "flo_skill_routing",
|
|
650
816
|
description:
|
|
@@ -837,6 +1003,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
837
1003
|
return asTextResult(parseMaybeJson(bodyText) || bodyText);
|
|
838
1004
|
}
|
|
839
1005
|
|
|
1006
|
+
if (name === "flo_query") {
|
|
1007
|
+
const query = requireString(args.query, "query");
|
|
1008
|
+
const prompt = `/flo:query ${query}`;
|
|
1009
|
+
const bodyText = await invokeInterfaceAgent(prompt, args.authToken);
|
|
1010
|
+
return asTextResult(parseMaybeJson(bodyText) || bodyText);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
840
1013
|
if (name === "flo_skill_routing") {
|
|
841
1014
|
const assetId = requireString(args.assetId, "assetId");
|
|
842
1015
|
const prompt = `/flo:skill-routing ${assetId}`;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const SERVER_KEY = "flo-plugin";
|
|
7
|
+
|
|
8
|
+
function defaultDesktopConfigPath() {
|
|
9
|
+
if (process.platform === "darwin") {
|
|
10
|
+
return path.join(
|
|
11
|
+
os.homedir(),
|
|
12
|
+
"Library",
|
|
13
|
+
"Application Support",
|
|
14
|
+
"Claude",
|
|
15
|
+
"claude_desktop_config.json"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (process.platform === "win32") {
|
|
19
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
20
|
+
return path.join(appData, "Claude", "claude_desktop_config.json");
|
|
21
|
+
}
|
|
22
|
+
return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readJson(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readFile(filePath, "utf8");
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedPluginEnv() {
|
|
35
|
+
const raw = (process.env.FLO_PLUGIN_ENV || "dev").trim().toLowerCase();
|
|
36
|
+
if (raw === "prod" || raw === "production") {
|
|
37
|
+
return "prod";
|
|
38
|
+
}
|
|
39
|
+
if (raw === "stg" || raw === "stage" || raw === "staging") {
|
|
40
|
+
return "stg";
|
|
41
|
+
}
|
|
42
|
+
return "dev";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultInvocationUrlForEnv() {
|
|
46
|
+
const env = normalizedPluginEnv();
|
|
47
|
+
if (env === "prod") {
|
|
48
|
+
return "https://plugin.floapp.co/invocations";
|
|
49
|
+
}
|
|
50
|
+
if (env === "stg") {
|
|
51
|
+
return "https://plugin.stg.floapp.co/invocations";
|
|
52
|
+
}
|
|
53
|
+
return "https://plugin.dev.floapp.co/invocations";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function defaultUserPoolNameForEnv() {
|
|
57
|
+
const env = normalizedPluginEnv();
|
|
58
|
+
if (env === "prod") {
|
|
59
|
+
return "flo-prod";
|
|
60
|
+
}
|
|
61
|
+
if (env === "stg") {
|
|
62
|
+
return "flo-stg";
|
|
63
|
+
}
|
|
64
|
+
return "flo-dev";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultPluginClientNameForEnv() {
|
|
68
|
+
const env = normalizedPluginEnv();
|
|
69
|
+
if (env === "prod") {
|
|
70
|
+
return "flo-prod-claude-plugin";
|
|
71
|
+
}
|
|
72
|
+
if (env === "stg") {
|
|
73
|
+
return "flo-stg-claude-plugin";
|
|
74
|
+
}
|
|
75
|
+
return "flo-dev-claude-plugin";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultSettingsUrlForEnv() {
|
|
79
|
+
const env = normalizedPluginEnv();
|
|
80
|
+
if (env === "prod") {
|
|
81
|
+
return "https://floapp.co/settings/api";
|
|
82
|
+
}
|
|
83
|
+
if (env === "stg") {
|
|
84
|
+
return "https://stg.floapp.co/settings/api";
|
|
85
|
+
}
|
|
86
|
+
return "https://dev.floapp.co/settings/api";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runAwsJson(args) {
|
|
90
|
+
const profile = (process.env.AWS_PROFILE || "").trim();
|
|
91
|
+
const cmdArgs = [...args];
|
|
92
|
+
if (profile) {
|
|
93
|
+
cmdArgs.push("--profile", profile);
|
|
94
|
+
}
|
|
95
|
+
const stdout = execFileSync("aws", cmdArgs, {
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
});
|
|
99
|
+
return JSON.parse(stdout);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function discoverOauthClient() {
|
|
103
|
+
const explicit = (process.env.FLO_OAUTH_CLIENT_ID || "").trim();
|
|
104
|
+
const region = (process.env.AWS_REGION || "us-east-1").trim();
|
|
105
|
+
const userPoolName = (
|
|
106
|
+
process.env.FLO_COGNITO_USER_POOL_NAME || defaultUserPoolNameForEnv()
|
|
107
|
+
).trim();
|
|
108
|
+
const preferredClientName = (
|
|
109
|
+
process.env.FLO_COGNITO_PLUGIN_CLIENT_NAME || defaultPluginClientNameForEnv()
|
|
110
|
+
).trim();
|
|
111
|
+
const fallbackClientName = (
|
|
112
|
+
process.env.FLO_COGNITO_FALLBACK_CLIENT_NAME || `${userPoolName}-spa`
|
|
113
|
+
).trim();
|
|
114
|
+
|
|
115
|
+
if (explicit) {
|
|
116
|
+
return {
|
|
117
|
+
clientId: explicit,
|
|
118
|
+
clientName: "(manual)",
|
|
119
|
+
userPoolId: (process.env.FLO_OAUTH_USER_POOL_ID || "").trim(),
|
|
120
|
+
userPoolName,
|
|
121
|
+
region,
|
|
122
|
+
settingsUrl:
|
|
123
|
+
(process.env.FLO_OAUTH_SETTINGS_URL || "").trim() ||
|
|
124
|
+
(process.env.FLO_OAUTH_CLIENT_ID_HELP_URL || "").trim() ||
|
|
125
|
+
defaultSettingsUrlForEnv(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pools = runAwsJson([
|
|
130
|
+
"cognito-idp",
|
|
131
|
+
"list-user-pools",
|
|
132
|
+
"--region",
|
|
133
|
+
region,
|
|
134
|
+
"--max-results",
|
|
135
|
+
"60",
|
|
136
|
+
"--output",
|
|
137
|
+
"json",
|
|
138
|
+
]);
|
|
139
|
+
const userPool = (pools.UserPools || []).find((p) => p?.Name === userPoolName);
|
|
140
|
+
if (!userPool?.Id) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Could not find Cognito user pool "${userPoolName}". Set FLO_OAUTH_CLIENT_ID manually.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const clients = runAwsJson([
|
|
147
|
+
"cognito-idp",
|
|
148
|
+
"list-user-pool-clients",
|
|
149
|
+
"--region",
|
|
150
|
+
region,
|
|
151
|
+
"--user-pool-id",
|
|
152
|
+
userPool.Id,
|
|
153
|
+
"--max-results",
|
|
154
|
+
"60",
|
|
155
|
+
"--output",
|
|
156
|
+
"json",
|
|
157
|
+
]);
|
|
158
|
+
const list = clients.UserPoolClients || [];
|
|
159
|
+
const preferred = list.find((c) => c?.ClientName === preferredClientName);
|
|
160
|
+
const fallback = list.find((c) => c?.ClientName === fallbackClientName);
|
|
161
|
+
const picked = preferred || fallback;
|
|
162
|
+
if (!picked?.ClientId) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Could not find Cognito app client "${preferredClientName}" (or "${fallbackClientName}") in pool "${userPoolName}". Set FLO_OAUTH_CLIENT_ID manually.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
clientId: picked.ClientId,
|
|
169
|
+
clientName: picked.ClientName || preferredClientName,
|
|
170
|
+
userPoolId: userPool.Id,
|
|
171
|
+
userPoolName,
|
|
172
|
+
region,
|
|
173
|
+
settingsUrl:
|
|
174
|
+
(process.env.FLO_OAUTH_SETTINGS_URL || "").trim() ||
|
|
175
|
+
(process.env.FLO_OAUTH_CLIENT_ID_HELP_URL || "").trim() ||
|
|
176
|
+
defaultSettingsUrlForEnv(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveInvocationUrl() {
|
|
181
|
+
if ((process.env.FLO_INTERFACE_AGENT_INVOCATION_URL || "").trim()) {
|
|
182
|
+
return process.env.FLO_INTERFACE_AGENT_INVOCATION_URL.trim();
|
|
183
|
+
}
|
|
184
|
+
return defaultInvocationUrlForEnv();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function resolveServerConfig() {
|
|
188
|
+
const rootDir = path.resolve(process.cwd());
|
|
189
|
+
const serverPath = path.join(rootDir, "packages", "flo-claude-plugin-mcp", "src", "index.js");
|
|
190
|
+
|
|
191
|
+
const invocationUrl = resolveInvocationUrl();
|
|
192
|
+
const authorizeUrl =
|
|
193
|
+
process.env.FLO_OAUTH_AUTHORIZE_URL ||
|
|
194
|
+
"https://flomenco-dev.auth.us-east-1.amazoncognito.com/oauth2/authorize";
|
|
195
|
+
const tokenUrl =
|
|
196
|
+
process.env.FLO_OAUTH_TOKEN_URL ||
|
|
197
|
+
"https://flomenco-dev.auth.us-east-1.amazoncognito.com/oauth2/token";
|
|
198
|
+
const oauthClient = discoverOauthClient();
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
command: "node",
|
|
202
|
+
args: [serverPath],
|
|
203
|
+
env: {
|
|
204
|
+
FLO_INTERFACE_AGENT_INVOCATION_URL: invocationUrl,
|
|
205
|
+
FLO_PLUGIN_ENV: normalizedPluginEnv(),
|
|
206
|
+
FLO_OAUTH_AUTHORIZE_URL: authorizeUrl,
|
|
207
|
+
FLO_OAUTH_TOKEN_URL: tokenUrl,
|
|
208
|
+
FLO_OAUTH_CLIENT_ID: oauthClient.clientId,
|
|
209
|
+
FLO_OAUTH_USER_POOL_ID: oauthClient.userPoolId,
|
|
210
|
+
FLO_OAUTH_USER_POOL_NAME: oauthClient.userPoolName,
|
|
211
|
+
FLO_OAUTH_EXPECTED_CLIENT_NAME: oauthClient.clientName,
|
|
212
|
+
FLO_OAUTH_SETTINGS_URL: oauthClient.settingsUrl,
|
|
213
|
+
FLO_OAUTH_REDIRECT_URI:
|
|
214
|
+
process.env.FLO_OAUTH_REDIRECT_URI || "http://127.0.0.1:8787/callback",
|
|
215
|
+
FLO_OAUTH_SCOPES: process.env.FLO_OAUTH_SCOPES || "openid email profile",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function main() {
|
|
221
|
+
const configPath = process.env.CLAUDE_DESKTOP_CONFIG_PATH || defaultDesktopConfigPath();
|
|
222
|
+
const config = await readJson(configPath);
|
|
223
|
+
config.mcpServers = config.mcpServers || {};
|
|
224
|
+
config.mcpServers[SERVER_KEY] = await resolveServerConfig();
|
|
225
|
+
|
|
226
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
227
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
228
|
+
|
|
229
|
+
console.log(`Installed "${SERVER_KEY}" into ${configPath}`);
|
|
230
|
+
const env = config.mcpServers[SERVER_KEY].env;
|
|
231
|
+
console.log(`OAuth client ID: ${env.FLO_OAUTH_CLIENT_ID}`);
|
|
232
|
+
console.log(`OAuth settings URL: ${env.FLO_OAUTH_SETTINGS_URL}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main().catch((err) => {
|
|
236
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function normalizedPluginEnv() {
|
|
4
|
+
const raw = (process.env.FLO_PLUGIN_ENV || "dev").trim().toLowerCase();
|
|
5
|
+
if (raw === "prod" || raw === "production") {
|
|
6
|
+
return "prod";
|
|
7
|
+
}
|
|
8
|
+
if (raw === "stg" || raw === "stage" || raw === "staging") {
|
|
9
|
+
return "stg";
|
|
10
|
+
}
|
|
11
|
+
return "dev";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function defaultInvocationUrlForEnv() {
|
|
15
|
+
const env = normalizedPluginEnv();
|
|
16
|
+
if (env === "prod") {
|
|
17
|
+
return "https://plugin.floapp.co/invocations";
|
|
18
|
+
}
|
|
19
|
+
if (env === "stg") {
|
|
20
|
+
return "https://plugin.stg.floapp.co/invocations";
|
|
21
|
+
}
|
|
22
|
+
return "https://plugin.dev.floapp.co/invocations";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function defaultUserPoolNameForEnv() {
|
|
26
|
+
const env = normalizedPluginEnv();
|
|
27
|
+
if (env === "prod") {
|
|
28
|
+
return "flo-prod";
|
|
29
|
+
}
|
|
30
|
+
if (env === "stg") {
|
|
31
|
+
return "flo-stg";
|
|
32
|
+
}
|
|
33
|
+
return "flo-dev";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultPluginClientNameForEnv() {
|
|
37
|
+
const env = normalizedPluginEnv();
|
|
38
|
+
if (env === "prod") {
|
|
39
|
+
return "flo-prod-claude-plugin";
|
|
40
|
+
}
|
|
41
|
+
if (env === "stg") {
|
|
42
|
+
return "flo-stg-claude-plugin";
|
|
43
|
+
}
|
|
44
|
+
return "flo-dev-claude-plugin";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultSettingsUrlForEnv() {
|
|
48
|
+
const env = normalizedPluginEnv();
|
|
49
|
+
if (env === "prod") {
|
|
50
|
+
return "https://floapp.co/settings/api";
|
|
51
|
+
}
|
|
52
|
+
if (env === "stg") {
|
|
53
|
+
return "https://stg.floapp.co/settings/api";
|
|
54
|
+
}
|
|
55
|
+
return "https://dev.floapp.co/settings/api";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runAwsJson(args) {
|
|
59
|
+
const profile = (process.env.AWS_PROFILE || "").trim();
|
|
60
|
+
const cmdArgs = [...args];
|
|
61
|
+
if (profile) {
|
|
62
|
+
cmdArgs.push("--profile", profile);
|
|
63
|
+
}
|
|
64
|
+
const stdout = execFileSync("aws", cmdArgs, {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
+
});
|
|
68
|
+
return JSON.parse(stdout);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function discoverOauthClient() {
|
|
72
|
+
const explicit = (process.env.FLO_OAUTH_CLIENT_ID || "").trim();
|
|
73
|
+
const region = (process.env.AWS_REGION || "us-east-1").trim();
|
|
74
|
+
const userPoolName = (
|
|
75
|
+
process.env.FLO_COGNITO_USER_POOL_NAME || defaultUserPoolNameForEnv()
|
|
76
|
+
).trim();
|
|
77
|
+
const preferredClientName = (
|
|
78
|
+
process.env.FLO_COGNITO_PLUGIN_CLIENT_NAME || defaultPluginClientNameForEnv()
|
|
79
|
+
).trim();
|
|
80
|
+
const fallbackClientName = (
|
|
81
|
+
process.env.FLO_COGNITO_FALLBACK_CLIENT_NAME || `${userPoolName}-spa`
|
|
82
|
+
).trim();
|
|
83
|
+
if (explicit) {
|
|
84
|
+
return {
|
|
85
|
+
clientId: explicit,
|
|
86
|
+
clientName: "(manual)",
|
|
87
|
+
userPoolId: (process.env.FLO_OAUTH_USER_POOL_ID || "").trim(),
|
|
88
|
+
userPoolName,
|
|
89
|
+
region,
|
|
90
|
+
settingsUrl:
|
|
91
|
+
(process.env.FLO_OAUTH_SETTINGS_URL || "").trim() ||
|
|
92
|
+
(process.env.FLO_OAUTH_CLIENT_ID_HELP_URL || "").trim() ||
|
|
93
|
+
defaultSettingsUrlForEnv(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const pools = runAwsJson([
|
|
98
|
+
"cognito-idp",
|
|
99
|
+
"list-user-pools",
|
|
100
|
+
"--region",
|
|
101
|
+
region,
|
|
102
|
+
"--max-results",
|
|
103
|
+
"60",
|
|
104
|
+
"--output",
|
|
105
|
+
"json",
|
|
106
|
+
]);
|
|
107
|
+
const userPool = (pools.UserPools || []).find((p) => p?.Name === userPoolName);
|
|
108
|
+
if (!userPool?.Id) {
|
|
109
|
+
throw new Error(`Unable to find user pool "${userPoolName}"`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const clients = runAwsJson([
|
|
113
|
+
"cognito-idp",
|
|
114
|
+
"list-user-pool-clients",
|
|
115
|
+
"--region",
|
|
116
|
+
region,
|
|
117
|
+
"--user-pool-id",
|
|
118
|
+
userPool.Id,
|
|
119
|
+
"--max-results",
|
|
120
|
+
"60",
|
|
121
|
+
"--output",
|
|
122
|
+
"json",
|
|
123
|
+
]);
|
|
124
|
+
const list = clients.UserPoolClients || [];
|
|
125
|
+
const preferred = list.find((c) => c?.ClientName === preferredClientName);
|
|
126
|
+
const fallback = list.find((c) => c?.ClientName === fallbackClientName);
|
|
127
|
+
const picked = preferred || fallback;
|
|
128
|
+
if (!picked?.ClientId) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Unable to find app client "${preferredClientName}" or "${fallbackClientName}"`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
clientId: picked.ClientId,
|
|
135
|
+
clientName: picked.ClientName || preferredClientName,
|
|
136
|
+
userPoolId: userPool.Id,
|
|
137
|
+
userPoolName,
|
|
138
|
+
region,
|
|
139
|
+
settingsUrl:
|
|
140
|
+
(process.env.FLO_OAUTH_SETTINGS_URL || "").trim() ||
|
|
141
|
+
(process.env.FLO_OAUTH_CLIENT_ID_HELP_URL || "").trim() ||
|
|
142
|
+
defaultSettingsUrlForEnv(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveInvocationUrl() {
|
|
147
|
+
if ((process.env.FLO_INTERFACE_AGENT_INVOCATION_URL || "").trim()) {
|
|
148
|
+
return process.env.FLO_INTERFACE_AGENT_INVOCATION_URL.trim();
|
|
149
|
+
}
|
|
150
|
+
return defaultInvocationUrlForEnv();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function main() {
|
|
154
|
+
const invocationUrl = resolveInvocationUrl();
|
|
155
|
+
const authorizeUrl =
|
|
156
|
+
process.env.FLO_OAUTH_AUTHORIZE_URL ||
|
|
157
|
+
"https://flomenco-dev.auth.us-east-1.amazoncognito.com/oauth2/authorize";
|
|
158
|
+
const tokenUrl =
|
|
159
|
+
process.env.FLO_OAUTH_TOKEN_URL ||
|
|
160
|
+
"https://flomenco-dev.auth.us-east-1.amazoncognito.com/oauth2/token";
|
|
161
|
+
const oauthClient = discoverOauthClient();
|
|
162
|
+
const redirectUri =
|
|
163
|
+
process.env.FLO_OAUTH_REDIRECT_URI || "http://127.0.0.1:8787/callback";
|
|
164
|
+
const scopes = process.env.FLO_OAUTH_SCOPES || "openid email profile";
|
|
165
|
+
|
|
166
|
+
const json = {
|
|
167
|
+
invocationUrl,
|
|
168
|
+
oauthAuthorizeUrl: authorizeUrl,
|
|
169
|
+
oauthTokenUrl: tokenUrl,
|
|
170
|
+
oauthClientId: oauthClient.clientId,
|
|
171
|
+
oauthClientName: oauthClient.clientName,
|
|
172
|
+
oauthUserPoolId: oauthClient.userPoolId,
|
|
173
|
+
oauthUserPoolName: oauthClient.userPoolName,
|
|
174
|
+
oauthSettingsUrl: oauthClient.settingsUrl,
|
|
175
|
+
oauthRedirectUri: redirectUri,
|
|
176
|
+
oauthScopes: scopes,
|
|
177
|
+
};
|
|
178
|
+
process.stdout.write(`${JSON.stringify(json, null, 2)}\n`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await main();
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
const SERVER_KEY = "flo-plugin";
|
|
6
|
+
|
|
7
|
+
function defaultDesktopConfigPath() {
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
return path.join(
|
|
10
|
+
os.homedir(),
|
|
11
|
+
"Library",
|
|
12
|
+
"Application Support",
|
|
13
|
+
"Claude",
|
|
14
|
+
"claude_desktop_config.json"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (process.platform === "win32") {
|
|
18
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
19
|
+
return path.join(appData, "Claude", "claude_desktop_config.json");
|
|
20
|
+
}
|
|
21
|
+
return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const configPath = process.env.CLAUDE_DESKTOP_CONFIG_PATH || defaultDesktopConfigPath();
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = await readFile(configPath, "utf8");
|
|
29
|
+
} catch {
|
|
30
|
+
console.log(`No Claude config found at ${configPath}; nothing to uninstall.`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config = JSON.parse(raw);
|
|
35
|
+
if (!config.mcpServers || !config.mcpServers[SERVER_KEY]) {
|
|
36
|
+
console.log(`"${SERVER_KEY}" not present in ${configPath}; nothing to uninstall.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
delete config.mcpServers[SERVER_KEY];
|
|
41
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
42
|
+
console.log(`Removed "${SERVER_KEY}" from ${configPath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
main().catch((err) => {
|
|
46
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
});
|