@assistant-ui/react-mcp 0.1.21 → 0.1.22
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.map +1 -1
- package/dist/auth/createOAuthProvider.js +7 -1
- package/dist/auth/createOAuthProvider.js.map +1 -1
- package/dist/primitives/addForm/McpAddFormRoot.d.ts.map +1 -1
- package/dist/primitives/addForm/McpAddFormRoot.js +14 -5
- package/dist/primitives/addForm/McpAddFormRoot.js.map +1 -1
- package/dist/resources/McpCustomServerPersistence.d.ts +17 -0
- package/dist/resources/McpCustomServerPersistence.d.ts.map +1 -0
- package/dist/resources/McpCustomServerPersistence.js +192 -0
- package/dist/resources/McpCustomServerPersistence.js.map +1 -0
- package/dist/resources/McpElicitationLifecycle.d.ts +12 -0
- package/dist/resources/McpElicitationLifecycle.d.ts.map +1 -0
- package/dist/resources/McpElicitationLifecycle.js +144 -0
- package/dist/resources/McpElicitationLifecycle.js.map +1 -0
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +28 -205
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerResource.d.ts.map +1 -1
- package/dist/resources/McpServerResource.js +24 -101
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/resources/storage/McpLocalStorage.d.ts +1 -4
- package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.js +1 -100
- package/dist/resources/storage/McpLocalStorage.js.map +1 -1
- package/dist/resources/storage/McpStoredDataNormalization.d.ts +5 -0
- package/dist/resources/storage/McpStoredDataNormalization.d.ts.map +1 -0
- package/dist/resources/storage/McpStoredDataNormalization.js +95 -0
- package/dist/resources/storage/McpStoredDataNormalization.js.map +1 -0
- package/dist/utils/serverUrl.d.ts +1 -0
- package/dist/utils/serverUrl.d.ts.map +1 -1
- package/dist/utils/serverUrl.js +15 -1
- package/dist/utils/serverUrl.js.map +1 -1
- package/package.json +10 -9
- package/src/auth/createOAuthProvider.test.ts +30 -1
- package/src/auth/createOAuthProvider.ts +12 -4
- package/src/primitives/addForm/McpAddFormRoot.test.tsx +66 -0
- package/src/primitives/addForm/McpAddFormRoot.tsx +11 -1
- package/src/resources/McpCustomServerPersistence.test.ts +56 -0
- package/src/resources/McpCustomServerPersistence.ts +233 -0
- package/src/resources/McpElicitationLifecycle.test.ts +83 -0
- package/src/resources/McpElicitationLifecycle.ts +156 -0
- package/src/resources/McpManagerResource.test.ts +36 -0
- package/src/resources/McpManagerResource.ts +25 -248
- package/src/resources/McpServerResource.test.ts +123 -10
- package/src/resources/McpServerResource.ts +29 -149
- package/src/resources/storage/McpLocalStorage.ts +9 -209
- package/src/resources/storage/McpStoredDataNormalization.test.ts +24 -0
- package/src/resources/storage/McpStoredDataNormalization.ts +201 -0
- package/src/utils/serverUrl.test.ts +30 -0
- package/src/utils/serverUrl.ts +26 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpLocalStorage.js","names":["resource","useMemo","OAuthMetadataSchema","OAuthClientInformationFullSchema","OAuthProtectedResourceMetadataSchema","OAuthTokensSchema","normalizeMcpServerUrl","MCPAuthConfig","MCPCustomServerRecord","MCPPersistedAuthState","assertValidServerId","MCPStorage","McpLocalStorageOptions","keyPrefix","storage","Storage","scopeId","resolveStorage","opts","globalThis","localStorage","isRecord","value","Record","Array","isArray","isOptionalString","undefined","isNonEmptyString","trim","length","isOptionalNonEmptyString","isOptionalStringArray","every","item","isOptionalConnectionTimeout","Number","isFinite","isValidServerId","id","isMCPAuthConfig","auth","type","token","scopes","authorizationEndpoint","tokenEndpoint","registrationEndpoint","clientId","clientSecret","isCustomServerRecord","name","url","createdAt","connectionTimeout","normalizeCustomServerRecord","record","normalizeCustomServerRecords","flatMap","normalizeOAuthTokens","result","safeParse","success","data","normalizeClientInformation","isSecureNetworkUrl","URL","isIpv4Loopback","test","hostname","protocol","endsWith","isMcpServerUrl","normalizeDiscoveryState","authorizationServerUrl","state","NonNullable","resourceMetadataUrl","metadata","authorizationServerMetadata","resourceMetadata","normalizePersistedAuthState","serverUrl","tokensClientId","codeVerifier","tokens","clientInformation","clientInformationSource","discoveryState","Object","keys","useMcpLocalStorage","t0","$","_c","t1","prefix","t2","customServersKey","t3","authKey","t4","key","fallback","raw","getItem","JSON","parse","T","read","t5","key_0","setItem","stringify","write","t6","key_1","removeItem","remove","t7","t8","t9","records","t10","id_0","t11","id_1","t12","id_2","t13","loadCustomServers","saveCustomServers","loadAuthState","saveAuthState","clearAuthState","McpLocalStorage","_c"],"sources":["../../../src/resources/storage/McpLocalStorage.ts"],"sourcesContent":["import { resource } from \"@assistant-ui/tap\";\nimport { useMemo } from \"react\";\nimport {\n OAuthMetadataSchema,\n OAuthClientInformationFullSchema,\n OAuthProtectedResourceMetadataSchema,\n OAuthTokensSchema,\n} from \"@modelcontextprotocol/core\";\nimport { normalizeMcpServerUrl } from \"../../utils/serverUrl\";\nimport type { MCPAuthConfig, MCPCustomServerRecord } from \"../../mcp-scope\";\nimport type { MCPPersistedAuthState } from \"../../auth/types\";\nimport { assertValidServerId } from \"../../utils/serverId\";\nimport type { MCPStorage } from \"./types\";\n\nexport type McpLocalStorageOptions = {\n /** Namespace prefix for keys. Default \"aui-mcp\". */\n keyPrefix?: string;\n /** Override the underlying Storage. Defaults to globalThis.localStorage. */\n storage?: Storage;\n /**\n * Stable identity for the backing data, used to key server reconnects.\n * Required to get reconnect-on-swap behavior when `storage` is overridden;\n * without it a custom backing store declares no scope, since a prefix\n * alone cannot distinguish two different stores.\n */\n scopeId?: string;\n};\n\nfunction resolveStorage(opts: McpLocalStorageOptions): Storage | null {\n if (opts.storage) return opts.storage;\n if (typeof globalThis !== \"undefined\" && \"localStorage\" in globalThis) {\n try {\n return (globalThis as { localStorage: Storage }).localStorage;\n } catch {\n return null;\n }\n }\n return null;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isOptionalString = (value: unknown): value is string | undefined =>\n value === undefined || typeof value === \"string\";\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === \"string\" && value.trim().length > 0;\n\nconst isOptionalNonEmptyString = (\n value: unknown,\n): value is string | undefined =>\n value === undefined || isNonEmptyString(value);\n\nconst isOptionalStringArray = (value: unknown): value is string[] | undefined =>\n value === undefined ||\n (Array.isArray(value) && value.every((item) => typeof item === \"string\"));\n\nconst isOptionalConnectionTimeout = (\n value: unknown,\n): value is number | undefined =>\n value === undefined ||\n (typeof value === \"number\" && Number.isFinite(value) && value >= 0);\n\nconst isValidServerId = (id: string): boolean => {\n try {\n assertValidServerId(id);\n return true;\n } catch {\n return false;\n }\n};\n\nconst isMCPAuthConfig = (auth: unknown): auth is MCPAuthConfig => {\n if (!isRecord(auth)) return false;\n\n switch (auth.type) {\n case \"none\":\n return true;\n case \"bearer\":\n return isOptionalNonEmptyString(auth.token);\n case \"oauth\":\n return (\n isOptionalStringArray(auth.scopes) &&\n isOptionalString(auth.authorizationEndpoint) &&\n isOptionalString(auth.tokenEndpoint) &&\n isOptionalString(auth.registrationEndpoint) &&\n isOptionalString(auth.clientId) &&\n isOptionalString(auth.clientSecret)\n );\n default:\n return false;\n }\n};\n\nconst isCustomServerRecord = (\n value: unknown,\n): value is MCPCustomServerRecord => {\n if (!isRecord(value)) return false;\n if (typeof value.id !== \"string\" || !isValidServerId(value.id)) {\n return false;\n }\n return (\n isNonEmptyString(value.name) &&\n isNonEmptyString(value.url) &&\n Number.isFinite(value.createdAt) &&\n isMCPAuthConfig(value.auth) &&\n isOptionalConnectionTimeout(value.connectionTimeout)\n );\n};\n\nconst normalizeCustomServerRecord = (\n value: unknown,\n): MCPCustomServerRecord | null => {\n if (isCustomServerRecord(value)) return value;\n if (!isRecord(value)) return null;\n\n const record = { ...value };\n delete record.connectionTimeout;\n return isCustomServerRecord(record) ? record : null;\n};\n\nexport const normalizeCustomServerRecords = (\n value: unknown,\n): MCPCustomServerRecord[] => {\n if (!Array.isArray(value)) return [];\n return value.flatMap((item) => {\n const record = normalizeCustomServerRecord(item);\n return record === null ? [] : [record];\n });\n};\n\nconst normalizeOAuthTokens = (\n value: unknown,\n): MCPPersistedAuthState[\"tokens\"] | undefined => {\n const result = OAuthTokensSchema.safeParse(value);\n return result.success ? result.data : undefined;\n};\n\nconst normalizeClientInformation = (\n value: unknown,\n): MCPPersistedAuthState[\"clientInformation\"] | undefined => {\n const result = OAuthClientInformationFullSchema.safeParse(value);\n return result.success ? result.data : undefined;\n};\n\nconst isSecureNetworkUrl = (value: unknown): value is string => {\n if (!isNonEmptyString(value)) return false;\n try {\n const url = new URL(value);\n const isIpv4Loopback = /^127(?:\\.\\d{1,3}){3}$/.test(url.hostname);\n return (\n url.protocol === \"https:\" ||\n (url.protocol === \"http:\" &&\n (url.hostname === \"localhost\" ||\n url.hostname.endsWith(\".localhost\") ||\n isIpv4Loopback ||\n url.hostname === \"[::1]\"))\n );\n } catch {\n return false;\n }\n};\n\nconst isMcpServerUrl = (value: unknown): value is string => {\n if (typeof value !== \"string\") return false;\n try {\n const url = new URL(value);\n return url.protocol === \"https:\" || url.protocol === \"http:\";\n } catch {\n return false;\n }\n};\n\nconst normalizeDiscoveryState = (\n value: unknown,\n): MCPPersistedAuthState[\"discoveryState\"] | undefined => {\n if (!isRecord(value) || !isSecureNetworkUrl(value.authorizationServerUrl)) {\n return undefined;\n }\n\n // A malformed optional field is dropped alone: keeping the validated\n // authorization server URL preserves the redirect-time binding, and the SDK\n // re-discovers whatever metadata is missing.\n const state: NonNullable<MCPPersistedAuthState[\"discoveryState\"]> = {\n authorizationServerUrl: value.authorizationServerUrl,\n };\n\n if (isSecureNetworkUrl(value.resourceMetadataUrl)) {\n state.resourceMetadataUrl = value.resourceMetadataUrl;\n }\n\n const metadata = OAuthMetadataSchema.safeParse(\n value.authorizationServerMetadata,\n );\n if (metadata.success) state.authorizationServerMetadata = metadata.data;\n\n const resourceMetadata = OAuthProtectedResourceMetadataSchema.safeParse(\n value.resourceMetadata,\n );\n if (resourceMetadata.success) state.resourceMetadata = resourceMetadata.data;\n\n return state;\n};\n\nexport const normalizePersistedAuthState = (\n value: unknown,\n): MCPPersistedAuthState | null => {\n if (!isRecord(value)) return null;\n if (\"serverUrl\" in value && !isMcpServerUrl(value.serverUrl)) return null;\n\n const state: MCPPersistedAuthState = {};\n if (isMcpServerUrl(value.serverUrl)) {\n state.serverUrl = normalizeMcpServerUrl(value.serverUrl);\n }\n if (isNonEmptyString(value.token)) state.token = value.token;\n if (isNonEmptyString(value.tokensClientId)) {\n state.tokensClientId = value.tokensClientId;\n }\n if (isNonEmptyString(value.codeVerifier)) {\n state.codeVerifier = value.codeVerifier;\n }\n if (isNonEmptyString(value.state)) state.state = value.state;\n\n const tokens = normalizeOAuthTokens(value.tokens);\n if (tokens) state.tokens = tokens;\n\n const clientInformation = normalizeClientInformation(value.clientInformation);\n if (clientInformation) state.clientInformation = clientInformation;\n if (value.clientInformationSource === \"registered\") {\n state.clientInformationSource = value.clientInformationSource;\n }\n\n const discoveryState = normalizeDiscoveryState(value.discoveryState);\n if (discoveryState) state.discoveryState = discoveryState;\n\n return Object.keys(state).length > 0 ? state : null;\n};\n\nconst useMcpLocalStorage = (opts: McpLocalStorageOptions = {}): MCPStorage => {\n const prefix = opts.keyPrefix ?? \"aui-mcp\";\n const storage = resolveStorage(opts);\n // Deriving a scope from the prefix is only honest for the shared\n // globalThis.localStorage; two custom backing stores under one prefix hold\n // different data, so an overridden backing declares no scope unless the\n // caller names one.\n const scopeId =\n opts.scopeId ??\n (opts.storage === undefined ? `local-storage:${prefix}` : undefined);\n\n // Callers key per-server coordination state on this instance, so it has to\n // stay referentially stable for as long as the underlying store does.\n return useMemo(() => {\n const customServersKey = `${prefix}:custom-servers`;\n const authKey = (id: string) => `${prefix}:auth:${id}`;\n\n const read = <T>(key: string, fallback: T): T => {\n if (!storage) return fallback;\n try {\n const raw = storage.getItem(key);\n if (raw == null) return fallback;\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n };\n\n const write = (key: string, value: unknown): void => {\n if (!storage) return;\n try {\n storage.setItem(key, JSON.stringify(value));\n } catch {\n // quota or serialization failure — silently drop\n }\n };\n\n const remove = (key: string): void => {\n if (!storage) return;\n try {\n storage.removeItem(key);\n } catch {\n // ignore\n }\n };\n\n return {\n ...(scopeId !== undefined ? { scopeId } : {}),\n loadCustomServers: async () =>\n normalizeCustomServerRecords(read<unknown>(customServersKey, [])),\n saveCustomServers: async (records) => {\n write(customServersKey, records);\n },\n loadAuthState: async (id) =>\n normalizePersistedAuthState(read<unknown>(authKey(id), null)),\n saveAuthState: async (id, state) => {\n write(authKey(id), state);\n },\n clearAuthState: async (id) => {\n remove(authKey(id));\n },\n };\n }, [prefix, storage, scopeId]);\n};\n\nexport const McpLocalStorage = resource(useMcpLocalStorage);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"McpLocalStorage.js","names":["resource","useMemo","MCPStorage","normalizeCustomServerRecords","normalizePersistedAuthState","McpLocalStorageOptions","keyPrefix","storage","Storage","scopeId","resolveStorage","opts","globalThis","localStorage","useMcpLocalStorage","t0","$","_c","t1","undefined","prefix","t2","customServersKey","t3","id","authKey","t4","key","fallback","raw","getItem","JSON","parse","T","read","t5","key_0","value","setItem","stringify","write","t6","key_1","removeItem","remove","t7","t8","t9","records","t10","id_0","t11","id_1","state","t12","id_2","t13","loadCustomServers","saveCustomServers","loadAuthState","saveAuthState","clearAuthState","McpLocalStorage","_c"],"sources":["../../../src/resources/storage/McpLocalStorage.ts"],"sourcesContent":["import { resource } from \"@assistant-ui/tap\";\nimport { useMemo } from \"react\";\nimport type { MCPStorage } from \"./types\";\nimport {\n normalizeCustomServerRecords,\n normalizePersistedAuthState,\n} from \"./McpStoredDataNormalization\";\n\nexport {\n normalizeCustomServerRecords,\n normalizePersistedAuthState,\n} from \"./McpStoredDataNormalization\";\n\nexport type McpLocalStorageOptions = {\n /** Namespace prefix for keys. Default \"aui-mcp\". */\n keyPrefix?: string;\n /** Override the underlying Storage. Defaults to globalThis.localStorage. */\n storage?: Storage;\n /**\n * Stable identity for the backing data, used to key server reconnects.\n * Required to get reconnect-on-swap behavior when `storage` is overridden;\n * without it a custom backing store declares no scope, since a prefix\n * alone cannot distinguish two different stores.\n */\n scopeId?: string;\n};\n\nfunction resolveStorage(opts: McpLocalStorageOptions): Storage | null {\n if (opts.storage) return opts.storage;\n if (typeof globalThis !== \"undefined\" && \"localStorage\" in globalThis) {\n try {\n return (globalThis as { localStorage: Storage }).localStorage;\n } catch {\n return null;\n }\n }\n return null;\n}\n\nconst useMcpLocalStorage = (opts: McpLocalStorageOptions = {}): MCPStorage => {\n const prefix = opts.keyPrefix ?? \"aui-mcp\";\n const storage = resolveStorage(opts);\n // Deriving a scope from the prefix is only honest for the shared\n // globalThis.localStorage; two custom backing stores under one prefix hold\n // different data, so an overridden backing declares no scope unless the\n // caller names one.\n const scopeId =\n opts.scopeId ??\n (opts.storage === undefined ? `local-storage:${prefix}` : undefined);\n\n // Callers key per-server coordination state on this instance, so it has to\n // stay referentially stable for as long as the underlying store does.\n return useMemo(() => {\n const customServersKey = `${prefix}:custom-servers`;\n const authKey = (id: string) => `${prefix}:auth:${id}`;\n\n const read = <T>(key: string, fallback: T): T => {\n if (!storage) return fallback;\n try {\n const raw = storage.getItem(key);\n if (raw == null) return fallback;\n return JSON.parse(raw) as T;\n } catch {\n return fallback;\n }\n };\n\n const write = (key: string, value: unknown): void => {\n if (!storage) return;\n try {\n storage.setItem(key, JSON.stringify(value));\n } catch {\n // quota or serialization failure — silently drop\n }\n };\n\n const remove = (key: string): void => {\n if (!storage) return;\n try {\n storage.removeItem(key);\n } catch {\n // ignore\n }\n };\n\n return {\n ...(scopeId !== undefined ? { scopeId } : {}),\n loadCustomServers: async () =>\n normalizeCustomServerRecords(read<unknown>(customServersKey, [])),\n saveCustomServers: async (records) => {\n write(customServersKey, records);\n },\n loadAuthState: async (id) =>\n normalizePersistedAuthState(read<unknown>(authKey(id), null)),\n saveAuthState: async (id, state) => {\n write(authKey(id), state);\n },\n clearAuthState: async (id) => {\n remove(authKey(id));\n },\n };\n }, [prefix, storage, scopeId]);\n};\n\nexport const McpLocalStorage = resource(useMcpLocalStorage);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MCPCustomServerRecord } from "../../mcp-scope.js";
|
|
2
|
+
import type { MCPPersistedAuthState } from "../../auth/types.js";
|
|
3
|
+
export declare const normalizeCustomServerRecords: (value: unknown) => MCPCustomServerRecord[];
|
|
4
|
+
export declare const normalizePersistedAuthState: (value: unknown) => MCPPersistedAuthState | null;
|
|
5
|
+
//# sourceMappingURL=McpStoredDataNormalization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpStoredDataNormalization.d.ts","sourceRoot":"","sources":["../../../src/resources/storage/McpStoredDataNormalization.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAiB,qBAAqB,EAAE,2BAAwB;AAC5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,4BAAyB;AAqF9D,eAAO,MAAM,4BAA4B,GACvC,OAAO,OAAO,KACb,qBAAqB,EAMvB,CAAC;AAgEF,eAAO,MAAM,2BAA2B,GACtC,OAAO,OAAO,KACb,qBAAqB,GAAG,IA8B1B,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isSecureNetworkUrl, normalizeMcpServerUrl } from "../../utils/serverUrl.js";
|
|
2
|
+
import { assertValidServerId } from "../../utils/serverId.js";
|
|
3
|
+
import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from "@modelcontextprotocol/core";
|
|
4
|
+
//#region src/resources/storage/McpStoredDataNormalization.ts
|
|
5
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
const isOptionalString = (value) => value === void 0 || typeof value === "string";
|
|
7
|
+
const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
8
|
+
const isOptionalNonEmptyString = (value) => value === void 0 || isNonEmptyString(value);
|
|
9
|
+
const isOptionalStringArray = (value) => value === void 0 || Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
10
|
+
const isOptionalConnectionTimeout = (value) => value === void 0 || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
11
|
+
const isValidServerId = (id) => {
|
|
12
|
+
try {
|
|
13
|
+
assertValidServerId(id);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const isMCPAuthConfig = (auth) => {
|
|
20
|
+
if (!isRecord(auth)) return false;
|
|
21
|
+
switch (auth.type) {
|
|
22
|
+
case "none": return true;
|
|
23
|
+
case "bearer": return isOptionalNonEmptyString(auth.token);
|
|
24
|
+
case "oauth": return isOptionalStringArray(auth.scopes) && isOptionalString(auth.authorizationEndpoint) && isOptionalString(auth.tokenEndpoint) && isOptionalString(auth.registrationEndpoint) && isOptionalString(auth.clientId) && isOptionalString(auth.clientSecret);
|
|
25
|
+
default: return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const isCustomServerRecord = (value) => {
|
|
29
|
+
if (!isRecord(value)) return false;
|
|
30
|
+
if (typeof value.id !== "string" || !isValidServerId(value.id)) return false;
|
|
31
|
+
return isNonEmptyString(value.name) && isNonEmptyString(value.url) && Number.isFinite(value.createdAt) && isMCPAuthConfig(value.auth) && isOptionalConnectionTimeout(value.connectionTimeout);
|
|
32
|
+
};
|
|
33
|
+
const normalizeCustomServerRecord = (value) => {
|
|
34
|
+
if (isCustomServerRecord(value)) return value;
|
|
35
|
+
if (!isRecord(value)) return null;
|
|
36
|
+
const record = { ...value };
|
|
37
|
+
delete record.connectionTimeout;
|
|
38
|
+
return isCustomServerRecord(record) ? record : null;
|
|
39
|
+
};
|
|
40
|
+
const normalizeCustomServerRecords = (value) => {
|
|
41
|
+
if (!Array.isArray(value)) return [];
|
|
42
|
+
return value.flatMap((item) => {
|
|
43
|
+
const record = normalizeCustomServerRecord(item);
|
|
44
|
+
return record === null ? [] : [record];
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const normalizeOAuthTokens = (value) => {
|
|
48
|
+
const result = OAuthTokensSchema.safeParse(value);
|
|
49
|
+
return result.success ? result.data : void 0;
|
|
50
|
+
};
|
|
51
|
+
const normalizeClientInformation = (value) => {
|
|
52
|
+
const result = OAuthClientInformationFullSchema.safeParse(value);
|
|
53
|
+
return result.success ? result.data : void 0;
|
|
54
|
+
};
|
|
55
|
+
const isMcpServerUrl = (value) => {
|
|
56
|
+
if (typeof value !== "string") return false;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(value);
|
|
59
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const normalizeDiscoveryState = (value) => {
|
|
65
|
+
if (!isRecord(value) || typeof value.authorizationServerUrl !== "string" || !isSecureNetworkUrl(value.authorizationServerUrl)) return;
|
|
66
|
+
const state = { authorizationServerUrl: value.authorizationServerUrl };
|
|
67
|
+
if (typeof value.resourceMetadataUrl === "string" && isSecureNetworkUrl(value.resourceMetadataUrl)) state.resourceMetadataUrl = value.resourceMetadataUrl;
|
|
68
|
+
const metadata = OAuthMetadataSchema.safeParse(value.authorizationServerMetadata);
|
|
69
|
+
if (metadata.success) state.authorizationServerMetadata = metadata.data;
|
|
70
|
+
const resourceMetadata = OAuthProtectedResourceMetadataSchema.safeParse(value.resourceMetadata);
|
|
71
|
+
if (resourceMetadata.success) state.resourceMetadata = resourceMetadata.data;
|
|
72
|
+
return state;
|
|
73
|
+
};
|
|
74
|
+
const normalizePersistedAuthState = (value) => {
|
|
75
|
+
if (!isRecord(value)) return null;
|
|
76
|
+
if ("serverUrl" in value && !isMcpServerUrl(value.serverUrl)) return null;
|
|
77
|
+
const state = {};
|
|
78
|
+
if (isMcpServerUrl(value.serverUrl)) state.serverUrl = normalizeMcpServerUrl(value.serverUrl);
|
|
79
|
+
if (isNonEmptyString(value.token)) state.token = value.token;
|
|
80
|
+
if (isNonEmptyString(value.tokensClientId)) state.tokensClientId = value.tokensClientId;
|
|
81
|
+
if (isNonEmptyString(value.codeVerifier)) state.codeVerifier = value.codeVerifier;
|
|
82
|
+
if (isNonEmptyString(value.state)) state.state = value.state;
|
|
83
|
+
const tokens = normalizeOAuthTokens(value.tokens);
|
|
84
|
+
if (tokens) state.tokens = tokens;
|
|
85
|
+
const clientInformation = normalizeClientInformation(value.clientInformation);
|
|
86
|
+
if (clientInformation) state.clientInformation = clientInformation;
|
|
87
|
+
if (value.clientInformationSource === "registered") state.clientInformationSource = value.clientInformationSource;
|
|
88
|
+
const discoveryState = normalizeDiscoveryState(value.discoveryState);
|
|
89
|
+
if (discoveryState) state.discoveryState = discoveryState;
|
|
90
|
+
return Object.keys(state).length > 0 ? state : null;
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
export { normalizeCustomServerRecords, normalizePersistedAuthState };
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=McpStoredDataNormalization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpStoredDataNormalization.js","names":["OAuthMetadataSchema","OAuthClientInformationFullSchema","OAuthProtectedResourceMetadataSchema","OAuthTokensSchema","isSecureNetworkUrl","normalizeMcpServerUrl","MCPAuthConfig","MCPCustomServerRecord","MCPPersistedAuthState","assertValidServerId","isRecord","value","Record","Array","isArray","isOptionalString","undefined","isNonEmptyString","trim","length","isOptionalNonEmptyString","isOptionalStringArray","every","item","isOptionalConnectionTimeout","Number","isFinite","isValidServerId","id","isMCPAuthConfig","auth","type","token","scopes","authorizationEndpoint","tokenEndpoint","registrationEndpoint","clientId","clientSecret","isCustomServerRecord","name","url","createdAt","connectionTimeout","normalizeCustomServerRecord","record","normalizeCustomServerRecords","flatMap","normalizeOAuthTokens","result","safeParse","success","data","normalizeClientInformation","isMcpServerUrl","URL","protocol","normalizeDiscoveryState","authorizationServerUrl","state","NonNullable","resourceMetadataUrl","metadata","authorizationServerMetadata","resourceMetadata","normalizePersistedAuthState","serverUrl","tokensClientId","codeVerifier","tokens","clientInformation","clientInformationSource","discoveryState","Object","keys"],"sources":["../../../src/resources/storage/McpStoredDataNormalization.ts"],"sourcesContent":["import {\n OAuthMetadataSchema,\n OAuthClientInformationFullSchema,\n OAuthProtectedResourceMetadataSchema,\n OAuthTokensSchema,\n} from \"@modelcontextprotocol/core\";\nimport {\n isSecureNetworkUrl,\n normalizeMcpServerUrl,\n} from \"../../utils/serverUrl\";\nimport type { MCPAuthConfig, MCPCustomServerRecord } from \"../../mcp-scope\";\nimport type { MCPPersistedAuthState } from \"../../auth/types\";\nimport { assertValidServerId } from \"../../utils/serverId\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isOptionalString = (value: unknown): value is string | undefined =>\n value === undefined || typeof value === \"string\";\n\nconst isNonEmptyString = (value: unknown): value is string =>\n typeof value === \"string\" && value.trim().length > 0;\n\nconst isOptionalNonEmptyString = (\n value: unknown,\n): value is string | undefined =>\n value === undefined || isNonEmptyString(value);\n\nconst isOptionalStringArray = (value: unknown): value is string[] | undefined =>\n value === undefined ||\n (Array.isArray(value) && value.every((item) => typeof item === \"string\"));\n\nconst isOptionalConnectionTimeout = (\n value: unknown,\n): value is number | undefined =>\n value === undefined ||\n (typeof value === \"number\" && Number.isFinite(value) && value >= 0);\n\nconst isValidServerId = (id: string): boolean => {\n try {\n assertValidServerId(id);\n return true;\n } catch {\n return false;\n }\n};\n\nconst isMCPAuthConfig = (auth: unknown): auth is MCPAuthConfig => {\n if (!isRecord(auth)) return false;\n\n switch (auth.type) {\n case \"none\":\n return true;\n case \"bearer\":\n return isOptionalNonEmptyString(auth.token);\n case \"oauth\":\n return (\n isOptionalStringArray(auth.scopes) &&\n isOptionalString(auth.authorizationEndpoint) &&\n isOptionalString(auth.tokenEndpoint) &&\n isOptionalString(auth.registrationEndpoint) &&\n isOptionalString(auth.clientId) &&\n isOptionalString(auth.clientSecret)\n );\n default:\n return false;\n }\n};\n\nconst isCustomServerRecord = (\n value: unknown,\n): value is MCPCustomServerRecord => {\n if (!isRecord(value)) return false;\n if (typeof value.id !== \"string\" || !isValidServerId(value.id)) {\n return false;\n }\n return (\n isNonEmptyString(value.name) &&\n isNonEmptyString(value.url) &&\n Number.isFinite(value.createdAt) &&\n isMCPAuthConfig(value.auth) &&\n isOptionalConnectionTimeout(value.connectionTimeout)\n );\n};\n\nconst normalizeCustomServerRecord = (\n value: unknown,\n): MCPCustomServerRecord | null => {\n if (isCustomServerRecord(value)) return value;\n if (!isRecord(value)) return null;\n\n const record = { ...value };\n delete record.connectionTimeout;\n return isCustomServerRecord(record) ? record : null;\n};\n\nexport const normalizeCustomServerRecords = (\n value: unknown,\n): MCPCustomServerRecord[] => {\n if (!Array.isArray(value)) return [];\n return value.flatMap((item) => {\n const record = normalizeCustomServerRecord(item);\n return record === null ? [] : [record];\n });\n};\n\nconst normalizeOAuthTokens = (\n value: unknown,\n): MCPPersistedAuthState[\"tokens\"] | undefined => {\n const result = OAuthTokensSchema.safeParse(value);\n return result.success ? result.data : undefined;\n};\n\nconst normalizeClientInformation = (\n value: unknown,\n): MCPPersistedAuthState[\"clientInformation\"] | undefined => {\n const result = OAuthClientInformationFullSchema.safeParse(value);\n return result.success ? result.data : undefined;\n};\n\nconst isMcpServerUrl = (value: unknown): value is string => {\n if (typeof value !== \"string\") return false;\n try {\n const url = new URL(value);\n return url.protocol === \"https:\" || url.protocol === \"http:\";\n } catch {\n return false;\n }\n};\n\nconst normalizeDiscoveryState = (\n value: unknown,\n): MCPPersistedAuthState[\"discoveryState\"] | undefined => {\n if (\n !isRecord(value) ||\n typeof value.authorizationServerUrl !== \"string\" ||\n !isSecureNetworkUrl(value.authorizationServerUrl)\n ) {\n return undefined;\n }\n\n // A malformed optional field is dropped alone: keeping the validated\n // authorization server URL preserves the redirect-time binding, and the SDK\n // re-discovers whatever metadata is missing.\n const state: NonNullable<MCPPersistedAuthState[\"discoveryState\"]> = {\n authorizationServerUrl: value.authorizationServerUrl,\n };\n\n if (\n typeof value.resourceMetadataUrl === \"string\" &&\n isSecureNetworkUrl(value.resourceMetadataUrl)\n ) {\n state.resourceMetadataUrl = value.resourceMetadataUrl;\n }\n\n const metadata = OAuthMetadataSchema.safeParse(\n value.authorizationServerMetadata,\n );\n if (metadata.success) state.authorizationServerMetadata = metadata.data;\n\n const resourceMetadata = OAuthProtectedResourceMetadataSchema.safeParse(\n value.resourceMetadata,\n );\n if (resourceMetadata.success) state.resourceMetadata = resourceMetadata.data;\n\n return state;\n};\n\nexport const normalizePersistedAuthState = (\n value: unknown,\n): MCPPersistedAuthState | null => {\n if (!isRecord(value)) return null;\n if (\"serverUrl\" in value && !isMcpServerUrl(value.serverUrl)) return null;\n\n const state: MCPPersistedAuthState = {};\n if (isMcpServerUrl(value.serverUrl)) {\n state.serverUrl = normalizeMcpServerUrl(value.serverUrl);\n }\n if (isNonEmptyString(value.token)) state.token = value.token;\n if (isNonEmptyString(value.tokensClientId)) {\n state.tokensClientId = value.tokensClientId;\n }\n if (isNonEmptyString(value.codeVerifier)) {\n state.codeVerifier = value.codeVerifier;\n }\n if (isNonEmptyString(value.state)) state.state = value.state;\n\n const tokens = normalizeOAuthTokens(value.tokens);\n if (tokens) state.tokens = tokens;\n\n const clientInformation = normalizeClientInformation(value.clientInformation);\n if (clientInformation) state.clientInformation = clientInformation;\n if (value.clientInformationSource === \"registered\") {\n state.clientInformationSource = value.clientInformationSource;\n }\n\n const discoveryState = normalizeDiscoveryState(value.discoveryState);\n if (discoveryState) state.discoveryState = discoveryState;\n\n return Object.keys(state).length > 0 ? state : null;\n};\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MCPPersistedAuthState } from "../auth/types.js";
|
|
2
2
|
export declare const normalizeMcpServerUrl: (serverUrl: string) => string;
|
|
3
|
+
export declare const isSecureNetworkUrl: (serverUrl: string) => boolean;
|
|
3
4
|
export declare const isAuthStateForServerUrl: (state: MCPPersistedAuthState | null, serverUrl: string) => boolean;
|
|
4
5
|
export declare const hasPersistedCredentials: (state: MCPPersistedAuthState | null) => boolean;
|
|
5
6
|
//# sourceMappingURL=serverUrl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverUrl.d.ts","sourceRoot":"","sources":["../../src/utils/serverUrl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,yBAAsB;AAE3D,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,KAAG,MAC3B,CAAC;
|
|
1
|
+
{"version":3,"file":"serverUrl.d.ts","sourceRoot":"","sources":["../../src/utils/serverUrl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,yBAAsB;AAE3D,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,KAAG,MAC3B,CAAC;AAgBhC,eAAO,MAAM,kBAAkB,GAAI,WAAW,MAAM,KAAG,OAUtD,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,OAAO,qBAAqB,GAAG,IAAI,EACnC,WAAW,MAAM,KAChB,OAUF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,OAAO,qBAAqB,GAAG,IAAI,KAClC,OAA0D,CAAC"}
|
package/dist/utils/serverUrl.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
//#region src/utils/serverUrl.ts
|
|
2
2
|
const normalizeMcpServerUrl = (serverUrl) => new URL(serverUrl).toString();
|
|
3
|
+
const IPV4_LOOPBACK = /^127(?:\.\d{1,3}){3}$/;
|
|
4
|
+
const IPV4_MAPPED_LOOPBACK = /^\[::ffff:7f[0-9a-f]{2}:[0-9a-f]{1,4}\]$/;
|
|
5
|
+
const isLoopbackHostname = (hostname) => {
|
|
6
|
+
const host = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
|
|
7
|
+
return host === "localhost" || host.endsWith(".localhost") || IPV4_LOOPBACK.test(host) || host === "[::1]" || IPV4_MAPPED_LOOPBACK.test(host);
|
|
8
|
+
};
|
|
9
|
+
const isSecureNetworkUrl = (serverUrl) => {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(serverUrl);
|
|
12
|
+
return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHostname(url.hostname);
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
3
17
|
const isAuthStateForServerUrl = (state, serverUrl) => {
|
|
4
18
|
if (state?.serverUrl === void 0) return false;
|
|
5
19
|
try {
|
|
@@ -10,6 +24,6 @@ const isAuthStateForServerUrl = (state, serverUrl) => {
|
|
|
10
24
|
};
|
|
11
25
|
const hasPersistedCredentials = (state) => Boolean(state?.tokens) || Boolean(state?.token);
|
|
12
26
|
//#endregion
|
|
13
|
-
export { hasPersistedCredentials, isAuthStateForServerUrl, normalizeMcpServerUrl };
|
|
27
|
+
export { hasPersistedCredentials, isAuthStateForServerUrl, isSecureNetworkUrl, normalizeMcpServerUrl };
|
|
14
28
|
|
|
15
29
|
//# sourceMappingURL=serverUrl.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverUrl.js","names":["MCPPersistedAuthState","normalizeMcpServerUrl","serverUrl","URL","toString","isAuthStateForServerUrl","state","undefined","hasPersistedCredentials","Boolean","tokens","token"],"sources":["../../src/utils/serverUrl.ts"],"sourcesContent":["import type { MCPPersistedAuthState } from \"../auth/types\";\n\nexport const normalizeMcpServerUrl = (serverUrl: string): string =>\n new URL(serverUrl).toString();\n\nexport const isAuthStateForServerUrl = (\n state: MCPPersistedAuthState | null,\n serverUrl: string,\n): boolean => {\n if (state?.serverUrl === undefined) return false;\n try {\n return (\n normalizeMcpServerUrl(state.serverUrl) ===\n normalizeMcpServerUrl(serverUrl)\n );\n } catch {\n return false;\n }\n};\n\nexport const hasPersistedCredentials = (\n state: MCPPersistedAuthState | null,\n): boolean => Boolean(state?.tokens) || Boolean(state?.token);\n"],"mappings":";AAEA,MAAaC,yBAAyBC,cACpC,IAAIC,IAAID,SAAS,CAAC,CAACE,SAAS;AAE9B,
|
|
1
|
+
{"version":3,"file":"serverUrl.js","names":["MCPPersistedAuthState","normalizeMcpServerUrl","serverUrl","URL","toString","IPV4_LOOPBACK","IPV4_MAPPED_LOOPBACK","isLoopbackHostname","hostname","host","endsWith","slice","test","isSecureNetworkUrl","url","protocol","isAuthStateForServerUrl","state","undefined","hasPersistedCredentials","Boolean","tokens","token"],"sources":["../../src/utils/serverUrl.ts"],"sourcesContent":["import type { MCPPersistedAuthState } from \"../auth/types\";\n\nexport const normalizeMcpServerUrl = (serverUrl: string): string =>\n new URL(serverUrl).toString();\n\nconst IPV4_LOOPBACK = /^127(?:\\.\\d{1,3}){3}$/;\nconst IPV4_MAPPED_LOOPBACK = /^\\[::ffff:7f[0-9a-f]{2}:[0-9a-f]{1,4}\\]$/;\n\nconst isLoopbackHostname = (hostname: string): boolean => {\n const host = hostname.endsWith(\".\") ? hostname.slice(0, -1) : hostname;\n return (\n host === \"localhost\" ||\n host.endsWith(\".localhost\") ||\n IPV4_LOOPBACK.test(host) ||\n host === \"[::1]\" ||\n IPV4_MAPPED_LOOPBACK.test(host)\n );\n};\n\nexport const isSecureNetworkUrl = (serverUrl: string): boolean => {\n try {\n const url = new URL(serverUrl);\n return (\n url.protocol === \"https:\" ||\n (url.protocol === \"http:\" && isLoopbackHostname(url.hostname))\n );\n } catch {\n return false;\n }\n};\n\nexport const isAuthStateForServerUrl = (\n state: MCPPersistedAuthState | null,\n serverUrl: string,\n): boolean => {\n if (state?.serverUrl === undefined) return false;\n try {\n return (\n normalizeMcpServerUrl(state.serverUrl) ===\n normalizeMcpServerUrl(serverUrl)\n );\n } catch {\n return false;\n }\n};\n\nexport const hasPersistedCredentials = (\n state: MCPPersistedAuthState | null,\n): boolean => Boolean(state?.tokens) || Boolean(state?.token);\n"],"mappings":";AAEA,MAAaC,yBAAyBC,cACpC,IAAIC,IAAID,SAAS,CAAC,CAACE,SAAS;AAE9B,MAAMC,gBAAgB;AACtB,MAAMC,uBAAuB;AAE7B,MAAMC,sBAAsBC,aAA8B;CACxD,MAAMC,OAAOD,SAASE,SAAS,GAAG,IAAIF,SAASG,MAAM,GAAG,EAAE,IAAIH;CAC9D,OACEC,SAAS,eACTA,KAAKC,SAAS,YAAY,KAC1BL,cAAcO,KAAKH,IAAI,KACvBA,SAAS,WACTH,qBAAqBM,KAAKH,IAAI;AAElC;AAEA,MAAaI,sBAAsBX,cAA+B;CAChE,IAAI;EACF,MAAMY,MAAM,IAAIX,IAAID,SAAS;EAC7B,OACEY,IAAIC,aAAa,YAChBD,IAAIC,aAAa,WAAWR,mBAAmBO,IAAIN,QAAQ;CAEhE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAaQ,2BACXC,OACAf,cACY;CACZ,IAAIe,OAAOf,cAAcgB,KAAAA,GAAW,OAAO;CAC3C,IAAI;EACF,OACEjB,sBAAsBgB,MAAMf,SAAS,MACrCD,sBAAsBC,SAAS;CAEnC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAaiB,2BACXF,UACYG,QAAQH,OAAOI,MAAM,KAAKD,QAAQH,OAAOK,KAAK"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "MCP server configuration and connection primitives for @assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@assistant-ui/core": "^0.3.
|
|
31
|
-
"@assistant-ui/store": "^0.3.
|
|
32
|
-
"@assistant-ui/tap": "^0.9.
|
|
33
|
-
"@modelcontextprotocol/client": "^2.
|
|
34
|
-
"@modelcontextprotocol/core": "^2.
|
|
35
|
-
"@radix-ui/react-primitive": "^2.1.
|
|
36
|
-
"assistant-stream": "^0.3.
|
|
30
|
+
"@assistant-ui/core": "^0.3.25",
|
|
31
|
+
"@assistant-ui/store": "^0.3.18",
|
|
32
|
+
"@assistant-ui/tap": "^0.9.22",
|
|
33
|
+
"@modelcontextprotocol/client": "^2.3.1",
|
|
34
|
+
"@modelcontextprotocol/core": "^2.3.1",
|
|
35
|
+
"@radix-ui/react-primitive": "^2.1.11",
|
|
36
|
+
"assistant-stream": "^0.3.49"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@types/react": "*",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@testing-library/dom": "^10.4.2",
|
|
51
51
|
"@testing-library/react": "^16.3.3",
|
|
52
52
|
"@types/react": "^19.3.0",
|
|
53
|
-
"jsdom": "^30.
|
|
53
|
+
"jsdom": "^30.1.2",
|
|
54
54
|
"react": "^19.3.0",
|
|
55
55
|
"vitest": "^5.0.3"
|
|
56
56
|
},
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "aui-build",
|
|
72
72
|
"test": "vitest run",
|
|
73
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
|
|
73
74
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
74
75
|
"test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
|
|
75
76
|
"test:types:peer-react18": "aui-react18-types",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { auth, type OAuthDiscoveryState } from "@modelcontextprotocol/client";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { MCPStorage } from "../resources/storage/types";
|
|
4
4
|
import type { MCPPersistedAuthState } from "./types";
|
|
5
5
|
import {
|
|
@@ -113,6 +113,10 @@ const rejectFetch = async () => {
|
|
|
113
113
|
throw new Error("Unexpected OAuth request");
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
vi.unstubAllGlobals();
|
|
118
|
+
});
|
|
119
|
+
|
|
116
120
|
describe("createOAuthProvider callback state", () => {
|
|
117
121
|
it("persists the generated state with the PKCE verifier", async () => {
|
|
118
122
|
const { storage, getState } = createStorage();
|
|
@@ -129,6 +133,31 @@ describe("createOAuthProvider callback state", () => {
|
|
|
129
133
|
});
|
|
130
134
|
});
|
|
131
135
|
|
|
136
|
+
it("uses secure random bytes when randomUUID is unavailable", async () => {
|
|
137
|
+
const getRandomValues = vi.fn((bytes: Uint8Array) => {
|
|
138
|
+
bytes.fill(0xab);
|
|
139
|
+
return bytes;
|
|
140
|
+
});
|
|
141
|
+
vi.stubGlobal("crypto", { getRandomValues });
|
|
142
|
+
const { storage } = createStorage();
|
|
143
|
+
const provider = createStaticProvider(storage);
|
|
144
|
+
|
|
145
|
+
expect(provider.state?.()).toBe(
|
|
146
|
+
"aui-mcp:ZG9jcw.q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s",
|
|
147
|
+
);
|
|
148
|
+
expect(getRandomValues).toHaveBeenCalledOnce();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("fails closed when secure randomness is unavailable", () => {
|
|
152
|
+
vi.stubGlobal("crypto", undefined);
|
|
153
|
+
const { storage } = createStorage();
|
|
154
|
+
const provider = createStaticProvider(storage);
|
|
155
|
+
|
|
156
|
+
expect(() => provider.state?.()).toThrow(
|
|
157
|
+
"Web Crypto is required to start MCP OAuth",
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
132
161
|
it("consumes callback state when tokens are saved", async () => {
|
|
133
162
|
const { storage, getState } = createStorage({
|
|
134
163
|
serverUrl,
|
|
@@ -36,6 +36,17 @@ function encodeServerIdInState(serverId: string): string {
|
|
|
36
36
|
return `${STATE_PREFIX}${bytesToBase64Url(bytes)}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function createStateNonce(): string {
|
|
40
|
+
if (typeof crypto === "undefined") {
|
|
41
|
+
throw new Error("Web Crypto is required to start MCP OAuth");
|
|
42
|
+
}
|
|
43
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
44
|
+
if (typeof crypto.getRandomValues !== "function") {
|
|
45
|
+
throw new Error("Web Crypto is required to start MCP OAuth");
|
|
46
|
+
}
|
|
47
|
+
return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
export function decodeServerIdFromState(state: string): string | null {
|
|
40
51
|
if (!state.startsWith(STATE_PREFIX)) return null;
|
|
41
52
|
const dot = state.indexOf(".", STATE_PREFIX.length);
|
|
@@ -376,10 +387,7 @@ export function createOAuthProvider(
|
|
|
376
387
|
state() {
|
|
377
388
|
// Embed the server id so the callback handler can route it back to the
|
|
378
389
|
// right MCPServerResource without app-level wiring.
|
|
379
|
-
const nonce =
|
|
380
|
-
typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
381
|
-
? crypto.randomUUID()
|
|
382
|
-
: `${Date.now()}.${Math.random()}`;
|
|
390
|
+
const nonce = createStateNonce();
|
|
383
391
|
pendingState = `${encodeServerIdInState(serverId)}.${nonce}`;
|
|
384
392
|
return pendingState;
|
|
385
393
|
},
|
|
@@ -102,6 +102,72 @@ describe("McpAddFormPrimitiveRoot", () => {
|
|
|
102
102
|
},
|
|
103
103
|
);
|
|
104
104
|
|
|
105
|
+
const submitAuthenticatedServer = (
|
|
106
|
+
url: string,
|
|
107
|
+
authType: "bearer" | "oauth" = "bearer",
|
|
108
|
+
) => {
|
|
109
|
+
render(
|
|
110
|
+
<McpAddFormPrimitiveRoot>
|
|
111
|
+
<McpAddFormPrimitiveNameField aria-label="Name" />
|
|
112
|
+
<McpAddFormPrimitiveUrlField aria-label="URL" />
|
|
113
|
+
<McpAddFormPrimitiveAuthSelect aria-label="Auth" />
|
|
114
|
+
<McpAddFormPrimitiveBearerTokenField aria-label="Token" />
|
|
115
|
+
<McpAddFormPrimitiveError />
|
|
116
|
+
<McpAddFormPrimitiveSubmit>Submit</McpAddFormPrimitiveSubmit>
|
|
117
|
+
</McpAddFormPrimitiveRoot>,
|
|
118
|
+
);
|
|
119
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
120
|
+
target: { value: "Docs" },
|
|
121
|
+
});
|
|
122
|
+
fireEvent.change(screen.getByLabelText("URL"), { target: { value: url } });
|
|
123
|
+
fireEvent.change(screen.getByLabelText("Auth"), {
|
|
124
|
+
target: { value: authType },
|
|
125
|
+
});
|
|
126
|
+
if (authType === "bearer") {
|
|
127
|
+
fireEvent.change(screen.getByLabelText("Token"), {
|
|
128
|
+
target: { value: "secret" },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
fireEvent.click(screen.getByRole("button", { name: "Submit" }));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
it.each(["bearer", "oauth"] as const)(
|
|
135
|
+
"rejects %s auth over plain HTTP to a remote host",
|
|
136
|
+
async (authType) => {
|
|
137
|
+
submitAuthenticatedServer("http://192.168.1.20:3000/mcp", authType);
|
|
138
|
+
|
|
139
|
+
expect(
|
|
140
|
+
await screen.findByText(
|
|
141
|
+
"Authenticated servers must use HTTPS or loopback HTTP",
|
|
142
|
+
),
|
|
143
|
+
).toBeTruthy();
|
|
144
|
+
expect(mocks.addCustomServer).not.toHaveBeenCalled();
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
it("clears the authenticated URL error when auth changes to none", async () => {
|
|
149
|
+
submitAuthenticatedServer("http://192.168.1.20:3000/mcp");
|
|
150
|
+
const message = "Authenticated servers must use HTTPS or loopback HTTP";
|
|
151
|
+
expect(await screen.findByText(message)).toBeTruthy();
|
|
152
|
+
|
|
153
|
+
fireEvent.change(screen.getByLabelText("Auth"), {
|
|
154
|
+
target: { value: "none" },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(screen.queryByText(message)).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("accepts bearer auth over loopback HTTP", async () => {
|
|
161
|
+
submitAuthenticatedServer("http://localhost:3000/mcp");
|
|
162
|
+
|
|
163
|
+
await waitFor(() => expect(mocks.addCustomServer).toHaveBeenCalledOnce());
|
|
164
|
+
expect(mocks.addCustomServer).toHaveBeenCalledWith({
|
|
165
|
+
name: "Docs",
|
|
166
|
+
url: "http://localhost:3000/mcp",
|
|
167
|
+
auth: { type: "bearer", token: "secret" },
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
105
171
|
it.each(["throws", "rejects"] as const)(
|
|
106
172
|
"does not turn a successful add into an error when onSubmitted %s",
|
|
107
173
|
async (mode) => {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./context";
|
|
18
18
|
import type { MCPAuthConfig } from "../../mcp-scope";
|
|
19
19
|
import { invokeMcpCallback } from "../../utils/invokeMcpCallback";
|
|
20
|
+
import { isSecureNetworkUrl } from "../../utils/serverUrl";
|
|
20
21
|
|
|
21
22
|
const INITIAL: AddFormState = {
|
|
22
23
|
name: "",
|
|
@@ -77,7 +78,8 @@ export const McpAddFormPrimitiveRoot = forwardRef<
|
|
|
77
78
|
const clearsError =
|
|
78
79
|
prev.errorField === "form" ||
|
|
79
80
|
prev.errorField === key ||
|
|
80
|
-
(key === "authType" &&
|
|
81
|
+
(key === "authType" &&
|
|
82
|
+
(prev.errorField === "bearerToken" || prev.errorField === "url"));
|
|
81
83
|
return {
|
|
82
84
|
...prev,
|
|
83
85
|
[key]: value,
|
|
@@ -136,6 +138,14 @@ export const McpAddFormPrimitiveRoot = forwardRef<
|
|
|
136
138
|
}));
|
|
137
139
|
return;
|
|
138
140
|
}
|
|
141
|
+
if (state.authType !== "none" && !isSecureNetworkUrl(urlResult.url)) {
|
|
142
|
+
setState((p) => ({
|
|
143
|
+
...p,
|
|
144
|
+
error: "Authenticated servers must use HTTPS or loopback HTTP",
|
|
145
|
+
errorField: "url",
|
|
146
|
+
}));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
139
149
|
setState((p) => ({
|
|
140
150
|
...p,
|
|
141
151
|
submitting: true,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import type { MCPCustomServerRecord } from "../mcp-scope";
|
|
4
|
+
import type { MCPStorage } from "./storage/types";
|
|
5
|
+
import { McpCustomServersResource } from "./McpCustomServerPersistence";
|
|
6
|
+
|
|
7
|
+
describe("McpCustomServersResource", () => {
|
|
8
|
+
it("merges a local mutation made before hydration and persists the result", async () => {
|
|
9
|
+
let resolveLoad: (records: MCPCustomServerRecord[]) => void = () => {};
|
|
10
|
+
const loaded = new Promise<MCPCustomServerRecord[]>((resolve) => {
|
|
11
|
+
resolveLoad = resolve;
|
|
12
|
+
});
|
|
13
|
+
const saveCustomServers = vi.fn(async () => {});
|
|
14
|
+
const storage: MCPStorage = {
|
|
15
|
+
loadCustomServers: () => loaded,
|
|
16
|
+
saveCustomServers,
|
|
17
|
+
loadAuthState: async () => null,
|
|
18
|
+
saveAuthState: async () => {},
|
|
19
|
+
clearAuthState: async () => {},
|
|
20
|
+
};
|
|
21
|
+
const persisted: MCPCustomServerRecord = {
|
|
22
|
+
id: "persisted",
|
|
23
|
+
name: "Persisted",
|
|
24
|
+
url: "https://example.com/persisted",
|
|
25
|
+
auth: { type: "none" },
|
|
26
|
+
createdAt: 1,
|
|
27
|
+
};
|
|
28
|
+
const local: MCPCustomServerRecord = {
|
|
29
|
+
...persisted,
|
|
30
|
+
id: "local",
|
|
31
|
+
name: "Local",
|
|
32
|
+
};
|
|
33
|
+
const persistenceQueues = new Map<string, Promise<void>>();
|
|
34
|
+
const root = createTapRoot(function Root() {
|
|
35
|
+
return useResource(
|
|
36
|
+
McpCustomServersResource({
|
|
37
|
+
storage,
|
|
38
|
+
scopeKey: "test",
|
|
39
|
+
persistenceQueues,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
root.getValue().updateCustomServers((records) => [...records, local]);
|
|
46
|
+
resolveLoad([persisted]);
|
|
47
|
+
await vi.waitFor(() => expect(root.getValue().isHydrated).toBe(true));
|
|
48
|
+
expect(root.getValue().customServers).toEqual([persisted, local]);
|
|
49
|
+
await vi.waitFor(() =>
|
|
50
|
+
expect(saveCustomServers).toHaveBeenCalledWith([persisted, local]),
|
|
51
|
+
);
|
|
52
|
+
} finally {
|
|
53
|
+
root.unmount();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|