@12-apps/mcp 1.15.1 → 1.16.1
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/package.json +7 -4
- package/src/react/ai-onboarding.tsx +26 -3
- package/src/react/ai-status-board.tsx +105 -14
- package/src/react/ai-steps.tsx +34 -9
- package/src/react/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic MCP server core: generate one MCP tool per OpenAPI operation and proxy each call to the endpoint carrying the caller's bearer token (permission passthrough). Also ships the reusable AI-connect onboarding UI at @12-apps/mcp/react.",
|
|
6
6
|
"exports": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@mui/icons-material": "^6.5.0",
|
|
20
|
-
"@12-apps/onboarding": "^1.
|
|
21
|
-
"@12-apps/ui": "^1.
|
|
20
|
+
"@12-apps/onboarding": "^1.16.1",
|
|
21
|
+
"@12-apps/ui": "^1.16.1",
|
|
22
22
|
"react": "^19.2.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
@@ -26,10 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@mui/material": "^6.5.0",
|
|
29
|
-
"@
|
|
29
|
+
"@testing-library/react": "^16.1.0",
|
|
30
|
+
"@12-apps/typescript-config": "^1.17.1",
|
|
30
31
|
"@types/node": "^22.15.3",
|
|
31
32
|
"@types/react": "19.2.2",
|
|
32
33
|
"eslint": "^9.39.1",
|
|
34
|
+
"jsdom": "^25.0.1",
|
|
35
|
+
"react-dom": "^19.2.0",
|
|
33
36
|
"typescript": "^5.8.2",
|
|
34
37
|
"vitest": "^3.2.4"
|
|
35
38
|
},
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type AiHostGuide,
|
|
18
18
|
} from "../guide";
|
|
19
19
|
import { AiLanding } from "./ai-landing";
|
|
20
|
+
import type { DisconnectHandler } from "./ai-status-board";
|
|
20
21
|
import {
|
|
21
22
|
buildFlowSteps,
|
|
22
23
|
connectedSummary,
|
|
@@ -57,6 +58,14 @@ export interface AiIntegrationOnboardingProps {
|
|
|
57
58
|
* `window.location.reload()`.
|
|
58
59
|
*/
|
|
59
60
|
onRetest?: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Revoke a connected assistant's access, from the completed status board. The
|
|
63
|
+
* host ID is the board's card, NOT the connection — `claude` and
|
|
64
|
+
* `claude-desktop` share one provider, so the app resolves which stored
|
|
65
|
+
* connection it owns (`providerForHostId`) and revokes that. Omitted → the
|
|
66
|
+
* board stays read-only, which is the pre-existing behavior.
|
|
67
|
+
*/
|
|
68
|
+
onDisconnect?: DisconnectHandler;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/** Resolved (defaults-applied) props threaded to the in-provider flow body. */
|
|
@@ -68,6 +77,7 @@ interface FlowProps {
|
|
|
68
77
|
permissionModel: string;
|
|
69
78
|
connectPrompt: string;
|
|
70
79
|
onRetest: () => void;
|
|
80
|
+
onDisconnect: DisconnectHandler | undefined;
|
|
71
81
|
devReset: boolean;
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -78,8 +88,17 @@ interface FlowProps {
|
|
|
78
88
|
* Escolher → Copiar URL → Configurar → Conectar → Confirmar flow.
|
|
79
89
|
*/
|
|
80
90
|
function AiOnboardingFlow(props: FlowProps): React.JSX.Element {
|
|
81
|
-
const {
|
|
82
|
-
|
|
91
|
+
const {
|
|
92
|
+
endpointUrl,
|
|
93
|
+
connections,
|
|
94
|
+
hosts,
|
|
95
|
+
capabilities,
|
|
96
|
+
permissionModel,
|
|
97
|
+
connectPrompt,
|
|
98
|
+
onRetest,
|
|
99
|
+
onDisconnect,
|
|
100
|
+
devReset,
|
|
101
|
+
} = props;
|
|
83
102
|
const { state } = useOnboarding();
|
|
84
103
|
const selectedHost = hosts.find((h) => h.id === state.data.selectedHost) ?? hosts[0]!;
|
|
85
104
|
|
|
@@ -97,7 +116,9 @@ function AiOnboardingFlow(props: FlowProps): React.JSX.Element {
|
|
|
97
116
|
configuredTitle={connectedTitle(connections, hosts, connectedHostId)}
|
|
98
117
|
configuredSummary={connectedSummary(connections)}
|
|
99
118
|
editLabel="Conectar IA"
|
|
100
|
-
completedContent={(nav) =>
|
|
119
|
+
completedContent={(nav) => (
|
|
120
|
+
<StatusBoard nav={nav} connections={connections} hosts={hosts} onDisconnect={onDisconnect} />
|
|
121
|
+
)}
|
|
101
122
|
devReset={devReset}
|
|
102
123
|
dataTestId="ai-onboarding"
|
|
103
124
|
/>
|
|
@@ -129,6 +150,7 @@ export function AiIntegrationOnboarding({
|
|
|
129
150
|
onRetest = () => {
|
|
130
151
|
if (typeof window !== "undefined") window.location.reload();
|
|
131
152
|
},
|
|
153
|
+
onDisconnect,
|
|
132
154
|
}: AiIntegrationOnboardingProps): React.JSX.Element {
|
|
133
155
|
return (
|
|
134
156
|
<OnboardingProvider featureKey={featureKey} store={store} initialState={initialState}>
|
|
@@ -140,6 +162,7 @@ export function AiIntegrationOnboarding({
|
|
|
140
162
|
permissionModel={permissionModel}
|
|
141
163
|
connectPrompt={connectPrompt}
|
|
142
164
|
onRetest={onRetest}
|
|
165
|
+
onDisconnect={onDisconnect}
|
|
143
166
|
devReset={devReset}
|
|
144
167
|
/>
|
|
145
168
|
</OnboardingProvider>
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import AddLinkOutlinedIcon from "@mui/icons-material/AddLinkOutlined";
|
|
4
4
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
|
5
|
+
import LinkOffOutlinedIcon from "@mui/icons-material/LinkOffOutlined";
|
|
6
|
+
import MenuBookOutlinedIcon from "@mui/icons-material/MenuBookOutlined";
|
|
5
7
|
|
|
8
|
+
import { ConfirmButton } from "@12-apps/ui/feedback/ConfirmAction";
|
|
6
9
|
import { Button } from "@12-apps/ui/form/Button";
|
|
7
10
|
import { Box } from "@12-apps/ui/mui/Box";
|
|
8
11
|
import { Stack } from "@12-apps/ui/mui/Stack";
|
|
@@ -11,6 +14,13 @@ import { Text } from "@12-apps/ui/typography/Text";
|
|
|
11
14
|
import type { AiHostGuide } from "../guide";
|
|
12
15
|
import { HostBrandAvatar } from "./ai-icons";
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Revoke a host's access. Returning a promise holds the confirmation popup in
|
|
19
|
+
* its pending state until the write settles, and a rejection keeps it open
|
|
20
|
+
* carrying the reason — so the owner sees the disconnect finish or fail.
|
|
21
|
+
*/
|
|
22
|
+
export type DisconnectHandler = (hostId: string) => void | Promise<unknown>;
|
|
23
|
+
|
|
14
24
|
/** One assistant's connection state. */
|
|
15
25
|
export interface HostStatus {
|
|
16
26
|
host: AiHostGuide;
|
|
@@ -19,13 +29,92 @@ export interface HostStatus {
|
|
|
19
29
|
detail?: string;
|
|
20
30
|
}
|
|
21
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The right-hand side of a CONNECTED box: the green pill, then the two things
|
|
34
|
+
* the pill alone left the owner unable to do.
|
|
35
|
+
*
|
|
36
|
+
* "Instruções" exists because the setup steps were reachable ONLY from a RED
|
|
37
|
+
* card's "Conectar" — so on an all-green board there was no way to re-read how
|
|
38
|
+
* any of them was connected, which is exactly when someone re-doing the setup on
|
|
39
|
+
* a second machine needs them. It re-enters the same flow that button does.
|
|
40
|
+
*
|
|
41
|
+
* "Desconectar" is only drawn when the app supplies a revoke path: a board with
|
|
42
|
+
* nowhere to send the revoke must not offer a button that does nothing.
|
|
43
|
+
*/
|
|
44
|
+
function ConnectedControls({
|
|
45
|
+
host,
|
|
46
|
+
onConnect,
|
|
47
|
+
onDisconnect,
|
|
48
|
+
}: {
|
|
49
|
+
host: AiHostGuide;
|
|
50
|
+
onConnect: (hostId: string) => void;
|
|
51
|
+
onDisconnect?: DisconnectHandler;
|
|
52
|
+
}): React.JSX.Element {
|
|
53
|
+
return (
|
|
54
|
+
<Stack
|
|
55
|
+
direction="row"
|
|
56
|
+
spacing={1}
|
|
57
|
+
alignItems="center"
|
|
58
|
+
justifyContent="flex-end"
|
|
59
|
+
// Wraps rather than squeezing: three controls do not fit a half-width
|
|
60
|
+
// card at every breakpoint.
|
|
61
|
+
sx={{ flexShrink: 0, flexWrap: "wrap", rowGap: 0.5 }}
|
|
62
|
+
>
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
px: 1,
|
|
66
|
+
py: 0.25,
|
|
67
|
+
borderRadius: 999,
|
|
68
|
+
bgcolor: "success.main",
|
|
69
|
+
color: "success.contrastText",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<Text variant="caption" weight="bold" as="span">
|
|
73
|
+
Conectado
|
|
74
|
+
</Text>
|
|
75
|
+
</Box>
|
|
76
|
+
<Button
|
|
77
|
+
variant="text"
|
|
78
|
+
size="sm"
|
|
79
|
+
onClick={() => onConnect(host.id)}
|
|
80
|
+
data-testid={`ai-status-instructions-${host.id}`}
|
|
81
|
+
>
|
|
82
|
+
<MenuBookOutlinedIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
|
83
|
+
Instruções
|
|
84
|
+
</Button>
|
|
85
|
+
{onDisconnect ? (
|
|
86
|
+
<ConfirmButton
|
|
87
|
+
variant="outline"
|
|
88
|
+
color="danger"
|
|
89
|
+
size="sm"
|
|
90
|
+
onClick={() => onDisconnect(host.id)}
|
|
91
|
+
confirm={{
|
|
92
|
+
title: "Desconectar o assistente?",
|
|
93
|
+
entityName: host.label,
|
|
94
|
+
description:
|
|
95
|
+
"Ele perde o acesso à sua loja na hora. Para voltar a usar, será preciso conectar de novo.",
|
|
96
|
+
confirmText: "Desconectar",
|
|
97
|
+
dataTestId: `ai-status-disconnect-confirm-${host.id}`,
|
|
98
|
+
}}
|
|
99
|
+
data-testid={`ai-status-disconnect-${host.id}`}
|
|
100
|
+
>
|
|
101
|
+
<LinkOffOutlinedIcon sx={{ fontSize: 16, mr: 0.5 }} />
|
|
102
|
+
Desconectar
|
|
103
|
+
</ConfirmButton>
|
|
104
|
+
) : null}
|
|
105
|
+
</Stack>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
22
109
|
/** A single green (connected) / red (to connect) status box for one assistant. */
|
|
23
110
|
function StatusBox({
|
|
24
111
|
status,
|
|
25
112
|
onConnect,
|
|
113
|
+
onDisconnect,
|
|
26
114
|
}: {
|
|
27
115
|
status: HostStatus;
|
|
28
116
|
onConnect: (hostId: string) => void;
|
|
117
|
+
onDisconnect?: DisconnectHandler;
|
|
29
118
|
}): React.JSX.Element {
|
|
30
119
|
const { host, connected, detail } = status;
|
|
31
120
|
return (
|
|
@@ -56,19 +145,7 @@ function StatusBox({
|
|
|
56
145
|
</Text>
|
|
57
146
|
</Box>
|
|
58
147
|
{connected ? (
|
|
59
|
-
<
|
|
60
|
-
sx={{
|
|
61
|
-
px: 1,
|
|
62
|
-
py: 0.25,
|
|
63
|
-
borderRadius: 999,
|
|
64
|
-
bgcolor: "success.main",
|
|
65
|
-
color: "success.contrastText",
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
<Text variant="caption" weight="bold" as="span">
|
|
69
|
-
Conectado
|
|
70
|
-
</Text>
|
|
71
|
-
</Box>
|
|
148
|
+
<ConnectedControls host={host} onConnect={onConnect} onDisconnect={onDisconnect} />
|
|
72
149
|
) : (
|
|
73
150
|
<Button
|
|
74
151
|
variant="outline"
|
|
@@ -90,13 +167,22 @@ function StatusBox({
|
|
|
90
167
|
* button that re-enters the guided flow for that host). Each connection is
|
|
91
168
|
* attributed to its provider (derived from the OAuth client + confirmed by
|
|
92
169
|
* `announceAiConnection`), so several assistants can show connected at once.
|
|
170
|
+
*
|
|
171
|
+
* A connected box carries two more controls, because the pill it used to show
|
|
172
|
+
* alone was a dead end in both directions: "Instruções" re-enters the host's
|
|
173
|
+
* setup steps (previously reachable only from a RED card, so an all-green board
|
|
174
|
+
* hid them entirely), and "Desconectar" — when the app passes `onDisconnect` —
|
|
175
|
+
* revokes access, which nothing in the UI could do at all.
|
|
93
176
|
*/
|
|
94
177
|
export function AiStatusBoard({
|
|
95
178
|
statuses,
|
|
96
179
|
onConnect,
|
|
180
|
+
onDisconnect,
|
|
97
181
|
}: {
|
|
98
182
|
statuses: readonly HostStatus[];
|
|
99
183
|
onConnect: (hostId: string) => void;
|
|
184
|
+
/** Revoke this host's access. Omit to render a read-only board. */
|
|
185
|
+
onDisconnect?: DisconnectHandler;
|
|
100
186
|
}): React.JSX.Element {
|
|
101
187
|
return (
|
|
102
188
|
<Stack spacing={2} data-testid="ai-status-board">
|
|
@@ -116,7 +202,12 @@ export function AiStatusBoard({
|
|
|
116
202
|
}}
|
|
117
203
|
>
|
|
118
204
|
{statuses.map((status) => (
|
|
119
|
-
<StatusBox
|
|
205
|
+
<StatusBox
|
|
206
|
+
key={status.host.id}
|
|
207
|
+
status={status}
|
|
208
|
+
onConnect={onConnect}
|
|
209
|
+
onDisconnect={onDisconnect}
|
|
210
|
+
/>
|
|
120
211
|
))}
|
|
121
212
|
</Box>
|
|
122
213
|
</Stack>
|
package/src/react/ai-steps.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
CopyUrlStep,
|
|
13
13
|
InstallStep,
|
|
14
14
|
} from "./ai-flow-steps";
|
|
15
|
-
import { AiStatusBoard, type HostStatus } from "./ai-status-board";
|
|
15
|
+
import { AiStatusBoard, type DisconnectHandler, type HostStatus } from "./ai-status-board";
|
|
16
16
|
import { HostSelectStep } from "./host-select-step";
|
|
17
17
|
|
|
18
18
|
export type { AiConnection } from "./ai-connection-utils";
|
|
@@ -23,10 +23,12 @@ export function StatusBoard({
|
|
|
23
23
|
nav,
|
|
24
24
|
connections,
|
|
25
25
|
hosts,
|
|
26
|
+
onDisconnect,
|
|
26
27
|
}: {
|
|
27
28
|
nav: GuidedNav;
|
|
28
29
|
connections: readonly AiConnection[];
|
|
29
30
|
hosts: readonly AiHostGuide[];
|
|
31
|
+
onDisconnect?: DisconnectHandler;
|
|
30
32
|
}): React.JSX.Element {
|
|
31
33
|
const legacyHostId = (nav.data.connectedHost ?? nav.data.selectedHost) as string | undefined;
|
|
32
34
|
|
|
@@ -45,15 +47,38 @@ export function StatusBoard({
|
|
|
45
47
|
};
|
|
46
48
|
});
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
/** Re-enter this host's setup — the manual copy path, or its one-click install. */
|
|
51
|
+
const goToConnect = (hostId: string): void => {
|
|
52
|
+
nav.goTo(hosts.find((h) => h.id === hostId)?.pluginUrl ? "install" : "copy", {
|
|
53
|
+
selectedHost: hostId,
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Disconnecting the LAST assistant leaves the section configured with nothing
|
|
59
|
+
* configured: a collapsed "IA conectada" summary hiding an all-red board, with
|
|
60
|
+
* the setup instructions two clicks away. So when the revoke empties the
|
|
61
|
+
* board, walk straight back into that host's instructions — the owner who just
|
|
62
|
+
* cut an assistant off is the likeliest person to reconnect one, and the steps
|
|
63
|
+
* are the only thing left worth showing.
|
|
64
|
+
*
|
|
65
|
+
* Cutting one of SEVERAL leaves the board meaningful, so it stays put.
|
|
66
|
+
* `statuses` is the pre-revoke picture, which is what makes "was that the last
|
|
67
|
+
* one?" answerable here without waiting for the app to re-read.
|
|
68
|
+
*/
|
|
69
|
+
const disconnect: DisconnectHandler | undefined = onDisconnect
|
|
70
|
+
? async (hostId) => {
|
|
71
|
+
await onDisconnect(hostId);
|
|
72
|
+
const revoked = providerForHostId(hostId);
|
|
73
|
+
const othersLive = statuses.some(
|
|
74
|
+
(status) => status.connected && providerForHostId(status.host.id) !== revoked,
|
|
75
|
+
);
|
|
76
|
+
if (!othersLive) goToConnect(hostId);
|
|
55
77
|
}
|
|
56
|
-
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<AiStatusBoard statuses={statuses} onConnect={goToConnect} onDisconnect={disconnect} />
|
|
57
82
|
);
|
|
58
83
|
}
|
|
59
84
|
|
package/src/react/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
// Building blocks + content, exported so apps can compose or override.
|
|
17
17
|
export { AiLanding } from "./ai-landing";
|
|
18
18
|
export { AiCapabilities } from "./ai-capabilities";
|
|
19
|
-
export { AiStatusBoard, type HostStatus } from "./ai-status-board";
|
|
19
|
+
export { AiStatusBoard, type DisconnectHandler, type HostStatus } from "./ai-status-board";
|
|
20
20
|
export { HostSelectStep } from "./host-select-step";
|
|
21
21
|
export {
|
|
22
22
|
EndpointCopyBlock,
|