@assistant-ui/react-mcp 0.1.15 → 0.1.17
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/createOAuthProvider.d.ts +19 -2
- package/dist/auth/createOAuthProvider.d.ts.map +1 -1
- package/dist/auth/createOAuthProvider.js +197 -35
- package/dist/auth/createOAuthProvider.js.map +1 -1
- package/dist/auth/types.d.ts +6 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.js +5 -6
- package/dist/hooks/useMcpOAuthCallback.js.map +1 -1
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +2 -1
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerResource.d.ts +2 -1
- package/dist/resources/McpServerResource.d.ts.map +1 -1
- package/dist/resources/McpServerResource.js +79 -19
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/resources/storage/McpLocalStorage.d.ts +7 -0
- package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.js +146 -37
- package/dist/resources/storage/McpLocalStorage.js.map +1 -1
- package/dist/resources/storage/McpMemoryStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpMemoryStorage.js +9 -6
- package/dist/resources/storage/McpMemoryStorage.js.map +1 -1
- package/dist/resources/storage/types.d.ts +12 -0
- package/dist/resources/storage/types.d.ts.map +1 -1
- package/dist/utils/serverUrl.d.ts +8 -0
- package/dist/utils/serverUrl.d.ts.map +1 -0
- package/dist/utils/serverUrl.js +15 -0
- package/dist/utils/serverUrl.js.map +1 -0
- package/package.json +7 -7
- package/src/auth/createOAuthProvider.test.ts +919 -5
- package/src/auth/createOAuthProvider.ts +328 -42
- package/src/auth/types.ts +6 -1
- package/src/hooks/useMcpOAuthCallback.test.ts +74 -1
- package/src/hooks/useMcpOAuthCallback.tsx +11 -8
- package/src/resources/McpManagerResource.ts +5 -1
- package/src/resources/McpServerResource.test.ts +612 -16
- package/src/resources/McpServerResource.ts +95 -23
- package/src/resources/storage/McpLocalStorage.test.ts +97 -1
- package/src/resources/storage/McpLocalStorage.ts +90 -47
- package/src/resources/storage/McpMemoryStorage.test.ts +99 -0
- package/src/resources/storage/McpMemoryStorage.ts +23 -17
- package/src/resources/storage/types.ts +12 -0
- package/src/utils/serverUrl.test.ts +66 -0
- package/src/utils/serverUrl.ts +23 -0
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { resource } from "@assistant-ui/tap";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { generateId } from "@assistant-ui/core";
|
|
2
4
|
import type { MCPCustomServerRecord } from "../../mcp-scope";
|
|
3
5
|
import type { MCPPersistedAuthState } from "../../auth/types";
|
|
4
6
|
import type { MCPStorage } from "./types";
|
|
5
7
|
|
|
6
|
-
const useMcpMemoryStorage = (): MCPStorage =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
const useMcpMemoryStorage = (): MCPStorage =>
|
|
9
|
+
useMemo(() => {
|
|
10
|
+
let servers: MCPCustomServerRecord[] = [];
|
|
11
|
+
const auth = new Map<string, MCPPersistedAuthState>();
|
|
12
|
+
return {
|
|
13
|
+
// Each memory store is its own private data: a distinct instance is a
|
|
14
|
+
// distinct scope, so replacing one keys a reconnect.
|
|
15
|
+
scopeId: `memory:${generateId()}`,
|
|
16
|
+
loadCustomServers: async () => [...servers],
|
|
17
|
+
saveCustomServers: async (records) => {
|
|
18
|
+
servers = [...records];
|
|
19
|
+
},
|
|
20
|
+
loadAuthState: async (id) => auth.get(id) ?? null,
|
|
21
|
+
saveAuthState: async (id, state) => {
|
|
22
|
+
auth.set(id, state);
|
|
23
|
+
},
|
|
24
|
+
clearAuthState: async (id) => {
|
|
25
|
+
auth.delete(id);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}, []);
|
|
23
29
|
|
|
24
30
|
export const McpMemoryStorage = resource(useMcpMemoryStorage);
|
|
@@ -3,6 +3,18 @@ import type { MCPCustomServerRecord } from "../../mcp-scope";
|
|
|
3
3
|
import type { MCPPersistedAuthState } from "../../auth/types";
|
|
4
4
|
|
|
5
5
|
export type MCPStorage = {
|
|
6
|
+
/**
|
|
7
|
+
* Stable identity of the backing store. Two storages with the same scopeId
|
|
8
|
+
* must read and write the same persisted data. When present, server
|
|
9
|
+
* connections and the OAuth write fence key on it, so swapping to a
|
|
10
|
+
* differently-scoped storage reconnects instead of leaving a live OAuth flow
|
|
11
|
+
* on the replaced store, and clearing through a same-scoped replacement still
|
|
12
|
+
* waits for writes queued against the storage it replaced. When absent, the
|
|
13
|
+
* fence falls back to object identity while connections never re-key at all,
|
|
14
|
+
* so a storage rebuilt on every render has to declare a scopeId; without one
|
|
15
|
+
* a clear runs unfenced against the writes queued by the object it replaced.
|
|
16
|
+
*/
|
|
17
|
+
scopeId?: string;
|
|
6
18
|
loadCustomServers: () => Promise<MCPCustomServerRecord[]>;
|
|
7
19
|
saveCustomServers: (records: MCPCustomServerRecord[]) => Promise<void>;
|
|
8
20
|
loadAuthState: (serverId: string) => Promise<MCPPersistedAuthState | null>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
hasPersistedCredentials,
|
|
4
|
+
isAuthStateForServerUrl,
|
|
5
|
+
normalizeMcpServerUrl,
|
|
6
|
+
} from "./serverUrl";
|
|
7
|
+
|
|
8
|
+
describe("normalizeMcpServerUrl", () => {
|
|
9
|
+
it("normalizes host case and the default port", () => {
|
|
10
|
+
expect(normalizeMcpServerUrl("https://MCP.Example.com:443/mcp")).toBe(
|
|
11
|
+
"https://mcp.example.com/mcp",
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("isAuthStateForServerUrl", () => {
|
|
17
|
+
it("matches equivalent spellings of the same endpoint", () => {
|
|
18
|
+
expect(
|
|
19
|
+
isAuthStateForServerUrl(
|
|
20
|
+
{ serverUrl: "https://MCP.Example.com/mcp" },
|
|
21
|
+
"https://mcp.example.com:443/mcp",
|
|
22
|
+
),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rejects a different endpoint, an unbound record, and no record", () => {
|
|
27
|
+
expect(
|
|
28
|
+
isAuthStateForServerUrl(
|
|
29
|
+
{ serverUrl: "https://a.example.com/mcp" },
|
|
30
|
+
"https://b.example.com/mcp",
|
|
31
|
+
),
|
|
32
|
+
).toBe(false);
|
|
33
|
+
expect(
|
|
34
|
+
isAuthStateForServerUrl({ token: "t" }, "https://a.example.com/mcp"),
|
|
35
|
+
).toBe(false);
|
|
36
|
+
expect(isAuthStateForServerUrl(null, "https://a.example.com/mcp")).toBe(
|
|
37
|
+
false,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects an unparsable URL on either side", () => {
|
|
42
|
+
expect(
|
|
43
|
+
isAuthStateForServerUrl({ serverUrl: "not a url" }, "https://a.test/mcp"),
|
|
44
|
+
).toBe(false);
|
|
45
|
+
expect(
|
|
46
|
+
isAuthStateForServerUrl({ serverUrl: "https://a.test/mcp" }, "not a url"),
|
|
47
|
+
).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("hasPersistedCredentials", () => {
|
|
52
|
+
it("counts bearer and OAuth tokens, not flow state", () => {
|
|
53
|
+
expect(hasPersistedCredentials({ token: "t" })).toBe(true);
|
|
54
|
+
expect(hasPersistedCredentials({ token: "" })).toBe(false);
|
|
55
|
+
expect(
|
|
56
|
+
hasPersistedCredentials({
|
|
57
|
+
tokens: { access_token: "a", token_type: "bearer" },
|
|
58
|
+
}),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
expect(hasPersistedCredentials({ codeVerifier: "v", state: "s" })).toBe(
|
|
61
|
+
false,
|
|
62
|
+
);
|
|
63
|
+
expect(hasPersistedCredentials({})).toBe(false);
|
|
64
|
+
expect(hasPersistedCredentials(null)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MCPPersistedAuthState } from "../auth/types";
|
|
2
|
+
|
|
3
|
+
export const normalizeMcpServerUrl = (serverUrl: string): string =>
|
|
4
|
+
new URL(serverUrl).toString();
|
|
5
|
+
|
|
6
|
+
export const isAuthStateForServerUrl = (
|
|
7
|
+
state: MCPPersistedAuthState | null,
|
|
8
|
+
serverUrl: string,
|
|
9
|
+
): boolean => {
|
|
10
|
+
if (state?.serverUrl === undefined) return false;
|
|
11
|
+
try {
|
|
12
|
+
return (
|
|
13
|
+
normalizeMcpServerUrl(state.serverUrl) ===
|
|
14
|
+
normalizeMcpServerUrl(serverUrl)
|
|
15
|
+
);
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const hasPersistedCredentials = (
|
|
22
|
+
state: MCPPersistedAuthState | null,
|
|
23
|
+
): boolean => Boolean(state?.tokens) || Boolean(state?.token);
|