@assistant-ui/react-mcp 0.1.18 → 0.1.19
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/dist/auth/buildHeaders.d.ts +1 -2
- package/dist/auth/buildHeaders.d.ts.map +1 -1
- package/dist/auth/createOAuthProvider.d.ts +5 -6
- package/dist/auth/createOAuthProvider.d.ts.map +1 -1
- package/dist/auth/types.d.ts +1 -2
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/connector.d.ts +1 -2
- package/dist/connector.d.ts.map +1 -1
- package/dist/context/McpConnectorByIndexProvider.d.ts +1 -2
- package/dist/context/McpConnectorByIndexProvider.d.ts.map +1 -1
- package/dist/context/McpCustomServerByIndexProvider.d.ts +1 -2
- package/dist/context/McpCustomServerByIndexProvider.d.ts.map +1 -1
- package/dist/context/McpServerByIdProvider.d.ts +1 -2
- package/dist/context/McpServerByIdProvider.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.d.ts +5 -6
- package/dist/hooks/useMcpOAuthCallback.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.js +4 -0
- package/dist/hooks/useMcpOAuthCallback.js.map +1 -1
- package/dist/mcp-scope.d.ts +14 -15
- package/dist/mcp-scope.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormAuthFields.d.ts +4 -4
- package/dist/primitives/addForm/McpAddFormAuthFields.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormAuthFields.js +18 -71
- package/dist/primitives/addForm/McpAddFormAuthFields.js.map +1 -1
- package/dist/primitives/addForm/McpAddFormAuthSelect.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormAuthSelect.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormBearerTokenField.d.ts +10 -0
- package/dist/primitives/addForm/McpAddFormBearerTokenField.d.ts.map +1 -0
- package/dist/primitives/addForm/McpAddFormBearerTokenField.js +50 -0
- package/dist/primitives/addForm/McpAddFormBearerTokenField.js.map +1 -0
- package/dist/primitives/addForm/McpAddFormCancel.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormCancel.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormError.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormError.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormNameField.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormNameField.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormRoot.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormRoot.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormScopesField.d.ts +10 -0
- package/dist/primitives/addForm/McpAddFormScopesField.d.ts.map +1 -0
- package/dist/primitives/addForm/McpAddFormScopesField.js +38 -0
- package/dist/primitives/addForm/McpAddFormScopesField.js.map +1 -0
- package/dist/primitives/addForm/McpAddFormSubmit.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormSubmit.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormUrlField.d.ts +2 -3
- package/dist/primitives/addForm/McpAddFormUrlField.d.ts.map +1 -1
- package/dist/primitives/addForm/context.d.ts +7 -8
- package/dist/primitives/addForm/context.d.ts.map +1 -1
- package/dist/primitives/addForm.d.ts +4 -2
- package/dist/primitives/addForm.js +5 -1
- package/dist/primitives/addForm.js.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveAccept.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveAccept.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveCancel.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveCancel.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveDecline.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveDecline.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveError.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveError.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveFields.d.ts +3 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveFields.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveItems.d.ts +3 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveItems.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveMessage.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveMessage.d.ts.map +1 -1
- package/dist/primitives/elicitation/McpElicitationPrimitiveRoot.d.ts +2 -3
- package/dist/primitives/elicitation/McpElicitationPrimitiveRoot.d.ts.map +1 -1
- package/dist/primitives/elicitation/context.d.ts +7 -8
- package/dist/primitives/elicitation/context.d.ts.map +1 -1
- package/dist/primitives/elicitation/initialElicitationDraft.d.ts +1 -2
- package/dist/primitives/elicitation/initialElicitationDraft.d.ts.map +1 -1
- package/dist/primitives/elicitation/prepareElicitationContent.d.ts +1 -2
- package/dist/primitives/elicitation/prepareElicitationContent.d.ts.map +1 -1
- package/dist/primitives/elicitation/prepareElicitationContent.js +5 -1
- package/dist/primitives/elicitation/prepareElicitationContent.js.map +1 -1
- package/dist/primitives/manager/McpManagerAddCustomTrigger.d.ts +2 -3
- package/dist/primitives/manager/McpManagerAddCustomTrigger.d.ts.map +1 -1
- package/dist/primitives/manager/McpManagerConnectors.d.ts +2 -3
- package/dist/primitives/manager/McpManagerConnectors.d.ts.map +1 -1
- package/dist/primitives/manager/McpManagerCustomServers.d.ts +2 -3
- package/dist/primitives/manager/McpManagerCustomServers.d.ts.map +1 -1
- package/dist/primitives/manager/McpManagerRoot.d.ts +2 -3
- package/dist/primitives/manager/McpManagerRoot.d.ts.map +1 -1
- package/dist/primitives/server/McpServerConnectButton.d.ts +2 -3
- package/dist/primitives/server/McpServerConnectButton.d.ts.map +1 -1
- package/dist/primitives/server/McpServerDisconnectButton.d.ts +2 -3
- package/dist/primitives/server/McpServerDisconnectButton.d.ts.map +1 -1
- package/dist/primitives/server/McpServerError.d.ts +2 -3
- package/dist/primitives/server/McpServerError.d.ts.map +1 -1
- package/dist/primitives/server/McpServerIcon.d.ts +2 -3
- package/dist/primitives/server/McpServerIcon.d.ts.map +1 -1
- package/dist/primitives/server/McpServerName.d.ts +2 -3
- package/dist/primitives/server/McpServerName.d.ts.map +1 -1
- package/dist/primitives/server/McpServerOAuthLink.d.ts +2 -3
- package/dist/primitives/server/McpServerOAuthLink.d.ts.map +1 -1
- package/dist/primitives/server/McpServerRemoveButton.d.ts +2 -3
- package/dist/primitives/server/McpServerRemoveButton.d.ts.map +1 -1
- package/dist/primitives/server/McpServerRoot.d.ts +2 -3
- package/dist/primitives/server/McpServerRoot.d.ts.map +1 -1
- package/dist/primitives/server/McpServerStatus.d.ts +2 -3
- package/dist/primitives/server/McpServerStatus.d.ts.map +1 -1
- package/dist/primitives/server/McpServerToolName.d.ts +2 -3
- package/dist/primitives/server/McpServerToolName.d.ts.map +1 -1
- package/dist/primitives/server/McpServerTools.d.ts +3 -4
- package/dist/primitives/server/McpServerTools.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.d.ts +2 -3
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +93 -62
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerRemovalFence.d.ts +2 -3
- package/dist/resources/McpServerRemovalFence.d.ts.map +1 -1
- package/dist/resources/McpServerResource.d.ts +3 -4
- package/dist/resources/McpServerResource.d.ts.map +1 -1
- package/dist/resources/McpServerResource.js +4 -4
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/resources/storage/McpCustomStorage.d.ts +1 -2
- package/dist/resources/storage/McpCustomStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.d.ts +4 -5
- package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpMemoryStorage.d.ts +1 -2
- package/dist/resources/storage/McpMemoryStorage.d.ts.map +1 -1
- package/dist/resources/storage/types.d.ts +2 -3
- package/dist/resources/storage/types.d.ts.map +1 -1
- package/dist/resources/validateElicitationContent.d.ts +1 -2
- package/dist/resources/validateElicitationContent.d.ts.map +1 -1
- package/dist/utils/createMcpId.d.ts +1 -2
- package/dist/utils/createMcpId.d.ts.map +1 -1
- package/dist/utils/invokeMcpCallback.d.ts +1 -2
- package/dist/utils/invokeMcpCallback.d.ts.map +1 -1
- package/dist/utils/serverId.d.ts +2 -3
- package/dist/utils/serverId.d.ts.map +1 -1
- package/dist/utils/serverUrl.d.ts +3 -4
- package/dist/utils/serverUrl.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/hooks/useMcpOAuthCallback.test.ts +53 -0
- package/src/hooks/useMcpOAuthCallback.tsx +12 -0
- package/src/index.ts +2 -1
- package/src/primitives/addForm/McpAddFormAccessibility.test.tsx +49 -0
- package/src/primitives/addForm/McpAddFormAuthFields.tsx +7 -15
- package/src/primitives/addForm/McpAddFormBearerTokenField.tsx +46 -0
- package/src/primitives/addForm/McpAddFormRoot.test.tsx +65 -0
- package/src/primitives/addForm/McpAddFormScopesField.tsx +34 -0
- package/src/primitives/addForm.ts +2 -0
- package/src/primitives/elicitation/prepareElicitationContent.ts +8 -6
- package/src/resources/McpManagerResource.test.ts +85 -0
- package/src/resources/McpManagerResource.ts +56 -23
- package/src/resources/McpServerResource.test.ts +9 -6
- package/src/resources/McpServerResource.ts +6 -5
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { FC } from "react";
|
|
2
|
+
import { McpAddFormPrimitiveBearerTokenField } from "./McpAddFormBearerTokenField";
|
|
3
|
+
import { McpAddFormPrimitiveScopesField } from "./McpAddFormScopesField";
|
|
2
4
|
import { type AddFormAuthType, useAddForm } from "./context";
|
|
3
5
|
|
|
4
6
|
export namespace McpAddFormPrimitiveAuthFields {
|
|
5
7
|
export type Props = {
|
|
6
8
|
/**
|
|
7
9
|
* Optional render override. Receives the current auth type so apps can render
|
|
8
|
-
* fully custom inputs
|
|
10
|
+
* fully custom inputs, bound to the form through `BearerTokenField` and
|
|
11
|
+
* `ScopesField`. Defaults to a minimal built-in for bearer / oauth.
|
|
9
12
|
*/
|
|
10
13
|
children?: FC<{ authType: AddFormAuthType }>;
|
|
11
14
|
};
|
|
@@ -14,7 +17,7 @@ export namespace McpAddFormPrimitiveAuthFields {
|
|
|
14
17
|
export const McpAddFormPrimitiveAuthFields: FC<
|
|
15
18
|
McpAddFormPrimitiveAuthFields.Props
|
|
16
19
|
> = ({ children }) => {
|
|
17
|
-
const { state, ids
|
|
20
|
+
const { state, ids } = useAddForm();
|
|
18
21
|
|
|
19
22
|
if (children) {
|
|
20
23
|
const Render = children;
|
|
@@ -30,15 +33,8 @@ export const McpAddFormPrimitiveAuthFields: FC<
|
|
|
30
33
|
>
|
|
31
34
|
Bearer token
|
|
32
35
|
</label>
|
|
33
|
-
<
|
|
36
|
+
<McpAddFormPrimitiveBearerTokenField
|
|
34
37
|
id={ids.bearerToken}
|
|
35
|
-
type="password"
|
|
36
|
-
value={state.bearerToken}
|
|
37
|
-
onChange={(e) => setField("bearerToken", e.target.value)}
|
|
38
|
-
aria-invalid={state.errorField === "bearerToken" ? true : undefined}
|
|
39
|
-
aria-describedby={
|
|
40
|
-
state.errorField === "bearerToken" ? ids.error : undefined
|
|
41
|
-
}
|
|
42
38
|
data-mcp-auth-field="bearer-token"
|
|
43
39
|
/>
|
|
44
40
|
</div>
|
|
@@ -51,12 +47,8 @@ export const McpAddFormPrimitiveAuthFields: FC<
|
|
|
51
47
|
<label htmlFor={ids.scopes} data-mcp-auth-field-label="oauth-scopes">
|
|
52
48
|
OAuth scopes
|
|
53
49
|
</label>
|
|
54
|
-
<
|
|
50
|
+
<McpAddFormPrimitiveScopesField
|
|
55
51
|
id={ids.scopes}
|
|
56
|
-
type="text"
|
|
57
|
-
placeholder="Scopes (space-separated, optional)"
|
|
58
|
-
value={state.scopes}
|
|
59
|
-
onChange={(e) => setField("scopes", e.target.value)}
|
|
60
52
|
data-mcp-auth-field="oauth-scopes"
|
|
61
53
|
/>
|
|
62
54
|
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentPropsWithoutRef,
|
|
3
|
+
type ComponentRef,
|
|
4
|
+
forwardRef,
|
|
5
|
+
} from "react";
|
|
6
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
7
|
+
import { useAddForm } from "./context";
|
|
8
|
+
|
|
9
|
+
export namespace McpAddFormPrimitiveBearerTokenField {
|
|
10
|
+
export type Element = ComponentRef<typeof Primitive.input>;
|
|
11
|
+
export type Props = Omit<
|
|
12
|
+
ComponentPropsWithoutRef<typeof Primitive.input>,
|
|
13
|
+
"value" | "onChange" | "type"
|
|
14
|
+
>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const McpAddFormPrimitiveBearerTokenField = forwardRef<
|
|
18
|
+
McpAddFormPrimitiveBearerTokenField.Element,
|
|
19
|
+
McpAddFormPrimitiveBearerTokenField.Props
|
|
20
|
+
>((props, ref) => {
|
|
21
|
+
const { state, ids, setField } = useAddForm();
|
|
22
|
+
return (
|
|
23
|
+
<Primitive.input
|
|
24
|
+
type="password"
|
|
25
|
+
{...props}
|
|
26
|
+
aria-invalid={
|
|
27
|
+
props["aria-invalid"] ??
|
|
28
|
+
(state.errorField === "bearerToken" ? true : undefined)
|
|
29
|
+
}
|
|
30
|
+
aria-describedby={
|
|
31
|
+
[
|
|
32
|
+
props["aria-describedby"],
|
|
33
|
+
state.errorField === "bearerToken" ? ids.error : null,
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(" ") || undefined
|
|
37
|
+
}
|
|
38
|
+
ref={ref}
|
|
39
|
+
value={state.bearerToken}
|
|
40
|
+
onChange={(e) => setField("bearerToken", e.target.value)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
McpAddFormPrimitiveBearerTokenField.displayName =
|
|
46
|
+
"McpAddFormPrimitive.BearerTokenField";
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
screen,
|
|
8
8
|
waitFor,
|
|
9
9
|
} from "@testing-library/react";
|
|
10
|
+
import type { ComponentProps } from "react";
|
|
10
11
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
12
|
|
|
12
13
|
const mocks = vi.hoisted(() => ({
|
|
@@ -20,12 +21,18 @@ vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
|
20
21
|
}),
|
|
21
22
|
}));
|
|
22
23
|
|
|
24
|
+
import { McpAddFormPrimitiveAuthFields } from "./McpAddFormAuthFields";
|
|
25
|
+
import { McpAddFormPrimitiveAuthSelect } from "./McpAddFormAuthSelect";
|
|
26
|
+
import { McpAddFormPrimitiveBearerTokenField } from "./McpAddFormBearerTokenField";
|
|
23
27
|
import { McpAddFormPrimitiveError } from "./McpAddFormError";
|
|
24
28
|
import { McpAddFormPrimitiveNameField } from "./McpAddFormNameField";
|
|
25
29
|
import { McpAddFormPrimitiveRoot } from "./McpAddFormRoot";
|
|
30
|
+
import { McpAddFormPrimitiveScopesField } from "./McpAddFormScopesField";
|
|
26
31
|
import { McpAddFormPrimitiveSubmit } from "./McpAddFormSubmit";
|
|
27
32
|
import { McpAddFormPrimitiveUrlField } from "./McpAddFormUrlField";
|
|
28
33
|
|
|
34
|
+
const CustomInput = (props: ComponentProps<"input">) => <input {...props} />;
|
|
35
|
+
|
|
29
36
|
describe("McpAddFormPrimitiveRoot", () => {
|
|
30
37
|
beforeEach(() => {
|
|
31
38
|
mocks.addCustomServer.mockReset();
|
|
@@ -37,6 +44,64 @@ describe("McpAddFormPrimitiveRoot", () => {
|
|
|
37
44
|
vi.restoreAllMocks();
|
|
38
45
|
});
|
|
39
46
|
|
|
47
|
+
it.each([
|
|
48
|
+
[
|
|
49
|
+
"bearer",
|
|
50
|
+
McpAddFormPrimitiveBearerTokenField,
|
|
51
|
+
"secret",
|
|
52
|
+
{ type: "bearer", token: "secret" },
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"oauth",
|
|
56
|
+
McpAddFormPrimitiveScopesField,
|
|
57
|
+
"read, write",
|
|
58
|
+
{ type: "oauth", scopes: ["read", "write"] },
|
|
59
|
+
],
|
|
60
|
+
] as const)(
|
|
61
|
+
"submits a custom %s input bound through its field part",
|
|
62
|
+
async (authType, Field, value, auth) => {
|
|
63
|
+
render(
|
|
64
|
+
<McpAddFormPrimitiveRoot>
|
|
65
|
+
<McpAddFormPrimitiveNameField aria-label="Name" />
|
|
66
|
+
<McpAddFormPrimitiveUrlField aria-label="URL" />
|
|
67
|
+
<McpAddFormPrimitiveAuthSelect aria-label="Auth" />
|
|
68
|
+
<McpAddFormPrimitiveAuthFields>
|
|
69
|
+
{({ authType: current }) =>
|
|
70
|
+
current === authType ? (
|
|
71
|
+
<Field asChild>
|
|
72
|
+
<CustomInput aria-label="Credential" />
|
|
73
|
+
</Field>
|
|
74
|
+
) : null
|
|
75
|
+
}
|
|
76
|
+
</McpAddFormPrimitiveAuthFields>
|
|
77
|
+
<McpAddFormPrimitiveError />
|
|
78
|
+
<McpAddFormPrimitiveSubmit>Submit</McpAddFormPrimitiveSubmit>
|
|
79
|
+
</McpAddFormPrimitiveRoot>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
83
|
+
target: { value: "Docs" },
|
|
84
|
+
});
|
|
85
|
+
fireEvent.change(screen.getByLabelText("URL"), {
|
|
86
|
+
target: { value: "https://example.com/mcp" },
|
|
87
|
+
});
|
|
88
|
+
fireEvent.change(screen.getByLabelText("Auth"), {
|
|
89
|
+
target: { value: authType },
|
|
90
|
+
});
|
|
91
|
+
fireEvent.change(screen.getByLabelText("Credential"), {
|
|
92
|
+
target: { value },
|
|
93
|
+
});
|
|
94
|
+
fireEvent.click(screen.getByRole("button", { name: "Submit" }));
|
|
95
|
+
|
|
96
|
+
await waitFor(() => expect(mocks.addCustomServer).toHaveBeenCalledOnce());
|
|
97
|
+
expect(mocks.addCustomServer).toHaveBeenCalledWith({
|
|
98
|
+
name: "Docs",
|
|
99
|
+
url: "https://example.com/mcp",
|
|
100
|
+
auth,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
40
105
|
it.each(["throws", "rejects"] as const)(
|
|
41
106
|
"does not turn a successful add into an error when onSubmitted %s",
|
|
42
107
|
async (mode) => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentPropsWithoutRef,
|
|
3
|
+
type ComponentRef,
|
|
4
|
+
forwardRef,
|
|
5
|
+
} from "react";
|
|
6
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
7
|
+
import { useAddForm } from "./context";
|
|
8
|
+
|
|
9
|
+
export namespace McpAddFormPrimitiveScopesField {
|
|
10
|
+
export type Element = ComponentRef<typeof Primitive.input>;
|
|
11
|
+
export type Props = Omit<
|
|
12
|
+
ComponentPropsWithoutRef<typeof Primitive.input>,
|
|
13
|
+
"value" | "onChange" | "type"
|
|
14
|
+
>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const McpAddFormPrimitiveScopesField = forwardRef<
|
|
18
|
+
McpAddFormPrimitiveScopesField.Element,
|
|
19
|
+
McpAddFormPrimitiveScopesField.Props
|
|
20
|
+
>((props, ref) => {
|
|
21
|
+
const { state, setField } = useAddForm();
|
|
22
|
+
return (
|
|
23
|
+
<Primitive.input
|
|
24
|
+
type="text"
|
|
25
|
+
placeholder="Scopes (space-separated, optional)"
|
|
26
|
+
{...props}
|
|
27
|
+
ref={ref}
|
|
28
|
+
value={state.scopes}
|
|
29
|
+
onChange={(e) => setField("scopes", e.target.value)}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
McpAddFormPrimitiveScopesField.displayName = "McpAddFormPrimitive.ScopesField";
|
|
@@ -3,6 +3,8 @@ export { McpAddFormPrimitiveNameField as NameField } from "./addForm/McpAddFormN
|
|
|
3
3
|
export { McpAddFormPrimitiveUrlField as UrlField } from "./addForm/McpAddFormUrlField";
|
|
4
4
|
export { McpAddFormPrimitiveAuthSelect as AuthSelect } from "./addForm/McpAddFormAuthSelect";
|
|
5
5
|
export { McpAddFormPrimitiveAuthFields as AuthFields } from "./addForm/McpAddFormAuthFields";
|
|
6
|
+
export { McpAddFormPrimitiveBearerTokenField as BearerTokenField } from "./addForm/McpAddFormBearerTokenField";
|
|
7
|
+
export { McpAddFormPrimitiveScopesField as ScopesField } from "./addForm/McpAddFormScopesField";
|
|
6
8
|
export { McpAddFormPrimitiveSubmit as Submit } from "./addForm/McpAddFormSubmit";
|
|
7
9
|
export { McpAddFormPrimitiveCancel as Cancel } from "./addForm/McpAddFormCancel";
|
|
8
10
|
export { McpAddFormPrimitiveError as Error } from "./addForm/McpAddFormError";
|
|
@@ -72,12 +72,14 @@ export const prepareElicitationContent = (
|
|
|
72
72
|
const contentWithBooleanDefaults = {
|
|
73
73
|
...candidateContent,
|
|
74
74
|
...Object.fromEntries(
|
|
75
|
-
missingRequiredBooleans.map(({ property }) =>
|
|
76
|
-
property
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
missingRequiredBooleans.map(({ property }) => {
|
|
76
|
+
const schema = properties[property];
|
|
77
|
+
const defaultValue = isRecord(schema) ? schema.default : undefined;
|
|
78
|
+
return [
|
|
79
|
+
property,
|
|
80
|
+
typeof defaultValue === "boolean" ? defaultValue : false,
|
|
81
|
+
];
|
|
82
|
+
}),
|
|
81
83
|
),
|
|
82
84
|
};
|
|
83
85
|
const validationErrors = validateElicitationContent(
|
|
@@ -92,6 +92,51 @@ describe("McpManagerResource server ids", () => {
|
|
|
92
92
|
expect(() => assertUniqueServerIds(["docs", "linear"])).not.toThrow();
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
it("keeps the first persisted custom server when ids are duplicated", async () => {
|
|
96
|
+
const docsServer: MCPCustomServerRecord = {
|
|
97
|
+
id: "docs",
|
|
98
|
+
name: "Docs",
|
|
99
|
+
url: "https://example.com/docs/mcp",
|
|
100
|
+
auth: { type: "none" },
|
|
101
|
+
createdAt: 1,
|
|
102
|
+
};
|
|
103
|
+
const saveCustomServers = vi.fn(async () => {});
|
|
104
|
+
const consoleError = vi
|
|
105
|
+
.spyOn(console, "error")
|
|
106
|
+
.mockImplementation(() => {});
|
|
107
|
+
const root = mount(
|
|
108
|
+
[],
|
|
109
|
+
McpCustomStorage({
|
|
110
|
+
loadCustomServers: vi.fn(async () => [
|
|
111
|
+
docsServer,
|
|
112
|
+
{ ...docsServer, name: "Duplicate Docs", createdAt: 2 },
|
|
113
|
+
]),
|
|
114
|
+
saveCustomServers,
|
|
115
|
+
loadAuthState: vi.fn(async () => null),
|
|
116
|
+
saveAuthState: vi.fn(async () => {}),
|
|
117
|
+
clearAuthState: vi.fn(async () => {}),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await vi.waitFor(() =>
|
|
123
|
+
expect(root.getValue().getState().isHydrated).toBe(true),
|
|
124
|
+
);
|
|
125
|
+
expect(root.getValue().getState().customServers).toHaveLength(1);
|
|
126
|
+
expect(root.getValue().getState().customServers[0]).toMatchObject({
|
|
127
|
+
id: "docs",
|
|
128
|
+
name: "Docs",
|
|
129
|
+
});
|
|
130
|
+
expect(saveCustomServers).not.toHaveBeenCalled();
|
|
131
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
132
|
+
'[assistant-ui/react-mcp] ignored duplicate custom server id "docs" loaded from storage',
|
|
133
|
+
);
|
|
134
|
+
} finally {
|
|
135
|
+
consoleError.mockRestore();
|
|
136
|
+
root.unmount();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
95
140
|
it("passes connector cache configuration to its client", async () => {
|
|
96
141
|
mocks.Client.mockClear();
|
|
97
142
|
const root = mount([
|
|
@@ -374,6 +419,46 @@ describe("McpManagerResource storage failures", () => {
|
|
|
374
419
|
});
|
|
375
420
|
|
|
376
421
|
describe("McpManagerResource storage ordering", () => {
|
|
422
|
+
it("preserves a removal made before custom server hydration finishes", async () => {
|
|
423
|
+
const docsServer: MCPCustomServerRecord = {
|
|
424
|
+
id: "docs",
|
|
425
|
+
name: "Docs",
|
|
426
|
+
url: "https://example.com/docs/mcp",
|
|
427
|
+
auth: { type: "none" },
|
|
428
|
+
createdAt: 1,
|
|
429
|
+
};
|
|
430
|
+
let resolveLoad!: (records: MCPCustomServerRecord[]) => void;
|
|
431
|
+
const load = new Promise<MCPCustomServerRecord[]>((resolve) => {
|
|
432
|
+
resolveLoad = resolve;
|
|
433
|
+
});
|
|
434
|
+
const saveCustomServers = vi.fn(async () => {});
|
|
435
|
+
const root = mount(
|
|
436
|
+
[],
|
|
437
|
+
McpCustomStorage({
|
|
438
|
+
loadCustomServers: vi.fn(() => load),
|
|
439
|
+
saveCustomServers,
|
|
440
|
+
loadAuthState: vi.fn(async () => null),
|
|
441
|
+
saveAuthState: vi.fn(async () => {}),
|
|
442
|
+
clearAuthState: vi.fn(async () => {}),
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await root.getValue().removeServer("docs");
|
|
448
|
+
resolveLoad([docsServer]);
|
|
449
|
+
|
|
450
|
+
await vi.waitFor(() =>
|
|
451
|
+
expect(root.getValue().getState().isHydrated).toBe(true),
|
|
452
|
+
);
|
|
453
|
+
expect(root.getValue().getState().customServers).toHaveLength(0);
|
|
454
|
+
await vi.waitFor(() =>
|
|
455
|
+
expect(saveCustomServers).toHaveBeenCalledWith([]),
|
|
456
|
+
);
|
|
457
|
+
} finally {
|
|
458
|
+
root.unmount();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
377
462
|
it("persists custom server updates in invocation order", async () => {
|
|
378
463
|
let resolveFirstSave: (() => void) | undefined;
|
|
379
464
|
const firstSave = new Promise<void>((resolve) => {
|
|
@@ -116,6 +116,20 @@ const holdCustomServerPersistence = (
|
|
|
116
116
|
return release;
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
+
const deduplicateCustomServers = (records: MCPCustomServerRecord[]) => {
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
return records.filter((record) => {
|
|
122
|
+
if (seen.has(record.id)) {
|
|
123
|
+
console.error(
|
|
124
|
+
`[assistant-ui/react-mcp] ignored duplicate custom server id "${record.id}" loaded from storage`,
|
|
125
|
+
);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
seen.add(record.id);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
119
133
|
type McpCustomServersResourceProps = {
|
|
120
134
|
storage: MCPStorage;
|
|
121
135
|
scopeKey: string;
|
|
@@ -137,6 +151,7 @@ const useMcpCustomServersResource = ({
|
|
|
137
151
|
"pending",
|
|
138
152
|
);
|
|
139
153
|
const hasPendingMutationRef = useRef(false);
|
|
154
|
+
const [removedBeforeHydration] = useState(() => new Set<string>());
|
|
140
155
|
const reportedBlockedPersistenceRef = useRef(false);
|
|
141
156
|
|
|
142
157
|
const hydrate = useEffectEvent(async (signal: { cancelled: boolean }) => {
|
|
@@ -151,7 +166,8 @@ const useMcpCustomServersResource = ({
|
|
|
151
166
|
|
|
152
167
|
let records: Awaited<ReturnType<typeof storage.loadCustomServers>>;
|
|
153
168
|
try {
|
|
154
|
-
|
|
169
|
+
const loadedRecords = await storage.loadCustomServers();
|
|
170
|
+
records = deduplicateCustomServers(loadedRecords);
|
|
155
171
|
} catch (error) {
|
|
156
172
|
if (!signal.cancelled) {
|
|
157
173
|
reportCustomStorageFailure("load", error);
|
|
@@ -164,16 +180,21 @@ const useMcpCustomServersResource = ({
|
|
|
164
180
|
}
|
|
165
181
|
return;
|
|
166
182
|
}
|
|
167
|
-
|
|
168
183
|
// Merge rather than replace so any addCustomServer calls that
|
|
169
184
|
// happened before hydration resolved aren't silently overwritten.
|
|
170
185
|
// Persisted order wins; pre-hydration locals append.
|
|
171
186
|
const hadPendingMutation = hasPendingMutationRef.current;
|
|
187
|
+
const hydratedRecords = records.filter(
|
|
188
|
+
(record) => !removedBeforeHydration.has(record.id),
|
|
189
|
+
);
|
|
172
190
|
const mergedRecords = (() => {
|
|
173
191
|
const prev = customServersRef.current;
|
|
174
|
-
if (prev.length === 0) return
|
|
175
|
-
const persistedIds = new Set(
|
|
176
|
-
return [
|
|
192
|
+
if (prev.length === 0) return hydratedRecords;
|
|
193
|
+
const persistedIds = new Set(hydratedRecords.map((r) => r.id));
|
|
194
|
+
return [
|
|
195
|
+
...hydratedRecords,
|
|
196
|
+
...prev.filter((r) => !persistedIds.has(r.id)),
|
|
197
|
+
];
|
|
177
198
|
})();
|
|
178
199
|
customServersRef.current = mergedRecords;
|
|
179
200
|
hydrationStateRef.current = "succeeded";
|
|
@@ -232,7 +253,22 @@ const useMcpCustomServersResource = ({
|
|
|
232
253
|
[persistenceQueues, scopeKey, storage],
|
|
233
254
|
);
|
|
234
255
|
|
|
235
|
-
|
|
256
|
+
const removeCustomServer = useCallback(
|
|
257
|
+
(id: string) => {
|
|
258
|
+
if (hydrationStateRef.current === "pending") {
|
|
259
|
+
removedBeforeHydration.add(id);
|
|
260
|
+
}
|
|
261
|
+
updateCustomServers((prev) => prev.filter((record) => record.id !== id));
|
|
262
|
+
},
|
|
263
|
+
[removedBeforeHydration, updateCustomServers],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
customServers,
|
|
268
|
+
isHydrated,
|
|
269
|
+
updateCustomServers,
|
|
270
|
+
removeCustomServer,
|
|
271
|
+
};
|
|
236
272
|
};
|
|
237
273
|
|
|
238
274
|
const McpCustomServersResource = resource(useMcpCustomServersResource);
|
|
@@ -252,16 +288,17 @@ const useMcpManagerResource = (
|
|
|
252
288
|
);
|
|
253
289
|
const storageScopeKey =
|
|
254
290
|
storage.scopeId === undefined ? "unscoped" : `scoped:${storage.scopeId}`;
|
|
255
|
-
const { customServers, isHydrated, updateCustomServers } =
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
291
|
+
const { customServers, isHydrated, updateCustomServers, removeCustomServer } =
|
|
292
|
+
useResource(
|
|
293
|
+
withKey(
|
|
294
|
+
storageScopeKey,
|
|
295
|
+
McpCustomServersResource({
|
|
296
|
+
storage,
|
|
297
|
+
scopeKey: storageScopeKey,
|
|
298
|
+
persistenceQueues,
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
265
302
|
|
|
266
303
|
const serverElements = useMemo(() => {
|
|
267
304
|
assertUniqueServerIds([
|
|
@@ -313,9 +350,7 @@ const useMcpManagerResource = (
|
|
|
313
350
|
? { elicitation: s.elicitation }
|
|
314
351
|
: {}),
|
|
315
352
|
onRemove: async () => {
|
|
316
|
-
|
|
317
|
-
prev.filter((record) => record.id !== s.id),
|
|
318
|
-
);
|
|
353
|
+
removeCustomServer(s.id);
|
|
319
354
|
},
|
|
320
355
|
},
|
|
321
356
|
() =>
|
|
@@ -332,7 +367,7 @@ const useMcpManagerResource = (
|
|
|
332
367
|
redirectUri,
|
|
333
368
|
autoConnect,
|
|
334
369
|
connectionTimeout,
|
|
335
|
-
|
|
370
|
+
removeCustomServer,
|
|
336
371
|
persistenceQueues,
|
|
337
372
|
storageScopeKey,
|
|
338
373
|
]);
|
|
@@ -450,9 +485,7 @@ const useMcpManagerResource = (
|
|
|
450
485
|
);
|
|
451
486
|
try {
|
|
452
487
|
await clearOAuthProviderAuthState(storage, id);
|
|
453
|
-
|
|
454
|
-
prev.filter((record) => record.id !== id),
|
|
455
|
-
);
|
|
488
|
+
removeCustomServer(id);
|
|
456
489
|
} catch (error) {
|
|
457
490
|
releasePersistence();
|
|
458
491
|
throw error;
|
|
@@ -113,8 +113,8 @@ const requestElicitation = (
|
|
|
113
113
|
client: any,
|
|
114
114
|
message: string,
|
|
115
115
|
requestedSchema: unknown,
|
|
116
|
-
context: { signal: AbortSignal } = {
|
|
117
|
-
signal: new AbortController().signal,
|
|
116
|
+
context: { mcpReq: { signal: AbortSignal } } = {
|
|
117
|
+
mcpReq: { signal: new AbortController().signal },
|
|
118
118
|
},
|
|
119
119
|
) => {
|
|
120
120
|
const handler = client.requestHandlers.get("elicitation/create");
|
|
@@ -156,7 +156,6 @@ const mount = (
|
|
|
156
156
|
cache?: { readonly defaultTtlMs?: number } | undefined;
|
|
157
157
|
elicitation?: boolean | undefined;
|
|
158
158
|
kind?: "connector" | "custom" | undefined;
|
|
159
|
-
storage?: MCPStorage | undefined;
|
|
160
159
|
onRemove?: (() => Promise<void>) | undefined;
|
|
161
160
|
},
|
|
162
161
|
onMount?: (server: ClientOutput<"mcpServer">) => void,
|
|
@@ -292,7 +291,9 @@ describe("McpServerResource automatic authentication", () => {
|
|
|
292
291
|
});
|
|
293
292
|
|
|
294
293
|
try {
|
|
295
|
-
await waitFor(
|
|
294
|
+
await waitFor(
|
|
295
|
+
() => vi.mocked(storage.loadAuthState).mock.calls.length > 0,
|
|
296
|
+
);
|
|
296
297
|
await flushMacrotask();
|
|
297
298
|
|
|
298
299
|
expect(root.getValue().getState()).toMatchObject({
|
|
@@ -338,7 +339,9 @@ describe("McpServerResource automatic authentication", () => {
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
try {
|
|
341
|
-
await waitFor(
|
|
342
|
+
await waitFor(
|
|
343
|
+
() => vi.mocked(storage.loadAuthState).mock.calls.length > 0,
|
|
344
|
+
);
|
|
342
345
|
await flushMacrotask();
|
|
343
346
|
|
|
344
347
|
expect(root.getValue().getState()).toMatchObject({
|
|
@@ -1474,7 +1477,7 @@ describe("McpServerResource elicitation", () => {
|
|
|
1474
1477
|
type: "object",
|
|
1475
1478
|
properties: {},
|
|
1476
1479
|
},
|
|
1477
|
-
{ signal: controller.signal },
|
|
1480
|
+
{ mcpReq: { signal: controller.signal } },
|
|
1478
1481
|
);
|
|
1479
1482
|
await waitForResourceUpdate(
|
|
1480
1483
|
() => root.getValue().getState().pendingElicitations.length === 1,
|
|
@@ -380,7 +380,7 @@ const useMcpServerResourceInstance = (
|
|
|
380
380
|
};
|
|
381
381
|
elicitationResolversRef.current.set(id, {
|
|
382
382
|
resolve,
|
|
383
|
-
signal: context.signal,
|
|
383
|
+
signal: context.mcpReq.signal,
|
|
384
384
|
onAbort,
|
|
385
385
|
requestedSchema,
|
|
386
386
|
});
|
|
@@ -395,10 +395,10 @@ const useMcpServerResourceInstance = (
|
|
|
395
395
|
]);
|
|
396
396
|
const entry = elicitationResolversRef.current.get(id);
|
|
397
397
|
if (entry) {
|
|
398
|
-
if (context.signal.aborted) {
|
|
398
|
+
if (context.mcpReq.signal.aborted) {
|
|
399
399
|
entry.onAbort();
|
|
400
400
|
} else {
|
|
401
|
-
context.signal.addEventListener("abort", entry.onAbort, {
|
|
401
|
+
context.mcpReq.signal.addEventListener("abort", entry.onAbort, {
|
|
402
402
|
once: true,
|
|
403
403
|
});
|
|
404
404
|
}
|
|
@@ -712,7 +712,7 @@ const useMcpServerResourceInstance = (
|
|
|
712
712
|
): readonly { property: string; message: string }[] | undefined => {
|
|
713
713
|
if (response.action === "accept") {
|
|
714
714
|
const entry = elicitationResolversRef.current.get(id);
|
|
715
|
-
if (!entry) return;
|
|
715
|
+
if (!entry) return undefined;
|
|
716
716
|
|
|
717
717
|
if (
|
|
718
718
|
typeof response.content !== "object" ||
|
|
@@ -752,10 +752,11 @@ const useMcpServerResourceInstance = (
|
|
|
752
752
|
content: response.content as ElicitResult["content"],
|
|
753
753
|
};
|
|
754
754
|
resolvePendingElicitation(id, result);
|
|
755
|
-
return;
|
|
755
|
+
return undefined;
|
|
756
756
|
}
|
|
757
757
|
|
|
758
758
|
resolvePendingElicitation(id, { action: response.action });
|
|
759
|
+
return undefined;
|
|
759
760
|
},
|
|
760
761
|
};
|
|
761
762
|
};
|