got something building and activating in a react app, at least!

This commit is contained in:
Glen Maddern 2025-03-22 22:29:16 +11:00
parent eca1e45363
commit e6a22d452e
5 changed files with 670 additions and 15 deletions

View file

@ -26,6 +26,7 @@
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "^22.13.10", "@types/node": "^22.13.10",
"@types/react": "^19.0.12",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",

15
pnpm-lock.yaml generated
View file

@ -24,6 +24,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.13.10 specifier: ^22.13.10
version: 22.13.10 version: 22.13.10
'@types/react':
specifier: ^19.0.12
version: 19.0.12
prettier: prettier:
specifier: ^3.5.3 specifier: ^3.5.3
version: 3.5.3 version: 3.5.3
@ -347,6 +350,9 @@ packages:
'@types/range-parser@1.2.7': '@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/react@19.0.12':
resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==}
'@types/send@0.17.4': '@types/send@0.17.4':
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
@ -473,6 +479,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
debug@2.6.9: debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies: peerDependencies:
@ -1340,6 +1349,10 @@ snapshots:
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
'@types/react@19.0.12':
dependencies:
csstype: 3.1.3
'@types/send@0.17.4': '@types/send@0.17.4':
dependencies: dependencies:
'@types/mime': 1.3.5 '@types/mime': 1.3.5
@ -1476,6 +1489,8 @@ snapshots:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
csstype@3.1.3: {}
debug@2.6.9: debug@2.6.9:
dependencies: dependencies:
ms: 2.0.0 ms: 2.0.0

View file

@ -1,11 +1,33 @@
import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Tool, JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useState, useRef } from "react";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
import { discoverOAuthMetadata, startAuthorization, exchangeAuthorization } from "@modelcontextprotocol/sdk/client/auth.js";
import { OAuthClientInformation, OAuthMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
export type UseMcpOptions = { export type UseMcpOptions = {
/** The /sse URL of your remote MCP server */ /** The /sse URL of your remote MCP server */
url: string, url: string,
/** OAuth client name for registration */
// more options here as I think of them clientName?: string,
/** OAuth client URI for registration */
clientUri?: string,
/** Custom callback URL for OAuth redirect (defaults to /oauth/callback on the current origin) */
callbackUrl?: string,
/** Storage key prefix for OAuth data (defaults to "mcp_auth") */
storageKeyPrefix?: string,
/** Custom configuration for the MCP client */
clientConfig?: {
name?: string,
version?: string,
},
/** Whether to enable debug logging */
debug?: boolean,
/** Auto retry connection if it fails, with delay in ms (default: false) */
autoRetry?: boolean | number,
/** Auto reconnect if connection is lost, with delay in ms (default: 3000) */
autoReconnect?: boolean | number,
} }
export type UseMcpResult = { export type UseMcpResult = {
@ -24,21 +46,515 @@ export type UseMcpResult = {
error?: string, error?: string,
/** All internal log messages */ /** All internal log messages */
log: {level: 'debug' | 'info' | 'warn' | 'error', message: string}[], log: {level: 'debug' | 'info' | 'warn' | 'error', message: string}[],
/** Call a tool on the MCP server */
callTool: (name: string, args?: Record<string, unknown>) => Promise<any>,
/** Manually retry connection if it's in a failed state */
retry: () => void,
/** Manually disconnect from the MCP server */
disconnect: () => void,
}
// more as i think of them /**
* Browser-compatible OAuth client provider for MCP
*/
class BrowserOAuthClientProvider {
private storageKeyPrefix: string;
private serverUrlHash: string;
private clientName: string;
private clientUri: string;
private callbackUrl: string;
constructor(
readonly serverUrl: string,
options: {
storageKeyPrefix?: string;
clientName?: string;
clientUri?: string;
callbackUrl?: string;
} = {}
) {
this.storageKeyPrefix = options.storageKeyPrefix || "mcp_auth";
this.serverUrlHash = this.hashString(serverUrl);
this.clientName = options.clientName || "MCP Browser Client";
this.clientUri = options.clientUri || window.location.origin;
this.callbackUrl = options.callbackUrl || new URL("/oauth/callback", window.location.origin).toString();
}
get redirectUrl(): string {
return this.callbackUrl;
}
get clientMetadata() {
return {
redirect_uris: [this.redirectUrl],
token_endpoint_auth_method: "none",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
client_name: this.clientName,
client_uri: this.clientUri,
};
}
private hashString(str: string): string {
// Simple hash function for browser environments
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(16);
}
private getKey(key: string): string {
return `${this.storageKeyPrefix}_${this.serverUrlHash}_${key}`;
}
async clientInformation(): Promise<OAuthClientInformation | undefined> {
const key = this.getKey("client_info");
const data = localStorage.getItem(key);
if (!data) return undefined;
try {
return JSON.parse(data) as OAuthClientInformation;
} catch (e) {
return undefined;
}
}
async saveClientInformation(clientInformation: OAuthClientInformation): Promise<void> {
const key = this.getKey("client_info");
localStorage.setItem(key, JSON.stringify(clientInformation));
}
async tokens(): Promise<OAuthTokens | undefined> {
const key = this.getKey("tokens");
const data = localStorage.getItem(key);
if (!data) return undefined;
try {
return JSON.parse(data) as OAuthTokens;
} catch (e) {
return undefined;
}
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
const key = this.getKey("tokens");
localStorage.setItem(key, JSON.stringify(tokens));
}
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
// Store the auth state for the popup flow
const stateKey = this.getKey("auth_state");
const state = Math.random().toString(36).substring(2);
localStorage.setItem(stateKey, state);
authorizationUrl.searchParams.set("state", state);
// Open the authorization URL in a popup window
const popup = window.open(authorizationUrl.toString(), "mcp_auth", "width=600,height=700");
if (!popup || popup.closed) {
console.warn("Popup blocked. Redirecting in the same window...");
window.location.href = authorizationUrl.toString();
}
}
async saveCodeVerifier(codeVerifier: string): Promise<void> {
const key = this.getKey("code_verifier");
localStorage.setItem(key, codeVerifier);
}
async codeVerifier(): Promise<string> {
const key = this.getKey("code_verifier");
const verifier = localStorage.getItem(key);
if (!verifier) {
throw new Error("No code verifier found in storage");
}
return verifier;
}
} }
/** /**
* useMcp is a React hook that connects to a remote MCP server, negotiates auth * useMcp is a React hook that connects to a remote MCP server, negotiates auth
* (including opening a popup window or new tab to complete the OAuth flow), * (including opening a popup window or new tab to complete the OAuth flow),
* and enables passing a list of tools (once loaded) to ai-sdk (using `useChat`). * and enables passing a list of tools (once loaded) to ai-sdk (using `useChat`).
*
* The authorization flow
*/ */
export function useMcp( export function useMcp(options: UseMcpOptions): UseMcpResult {
options: UseMcpOptions const [state, setState] = useState<UseMcpResult['state']>('discovering');
):UseMcpResult { const [tools, setTools] = useState<Tool[]>([]);
// TODO: implement hook const [error, setError] = useState<string | undefined>(undefined);
const [log, setLog] = useState<UseMcpResult['log']>([]);
const clientRef = useRef<Client | null>(null);
const transportRef = useRef<SSEClientTransport | null>(null);
const authProviderRef = useRef<BrowserOAuthClientProvider | null>(null);
const metadataRef = useRef<OAuthMetadata | undefined>(undefined);
const connectingRef = useRef<boolean>(false);
const isInitialMount = useRef<boolean>(true);
// Set up default options
const {
url,
clientName = "MCP React Client",
clientUri = window.location.origin,
callbackUrl = new URL("/oauth/callback", window.location.origin).toString(),
storageKeyPrefix = "mcp_auth",
clientConfig = {
name: "mcp-react-client",
version: "0.1.0",
},
debug = false,
autoRetry = false,
autoReconnect = 3000,
} = options;
// Add to log
const addLog = useCallback((level: 'debug' | 'info' | 'warn' | 'error', message: string) => {
if (level === 'debug' && !debug) return;
setLog(prevLog => [...prevLog, { level, message }]);
}, [debug]);
// Call a tool on the MCP server
const callTool = useCallback(async (name: string, args?: Record<string, unknown>) => {
if (!clientRef.current || state !== 'ready') {
throw new Error("MCP client not ready");
}
try {
const result = await clientRef.current.request(
{
method: "tools/call",
params: { name, arguments: args },
}
);
return result;
} catch (err) {
addLog('error', `Error calling tool ${name}: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
}, [state, addLog]);
// Disconnect from the MCP server
const disconnect = useCallback(async () => {
if (clientRef.current) {
try {
await clientRef.current.close();
} catch (err) {
addLog('error', `Error closing client: ${err instanceof Error ? err.message : String(err)}`);
}
clientRef.current = null;
}
if (transportRef.current) {
try {
await transportRef.current.close();
} catch (err) {
addLog('error', `Error closing transport: ${err instanceof Error ? err.message : String(err)}`);
}
transportRef.current = null;
}
connectingRef.current = false;
setState('discovering');
setTools([]);
setError(undefined);
}, [addLog]);
let handleAuthentication: () => Promise<string>;
// Initialize connection to MCP server
const connect = useCallback(async () => {
// Prevent multiple simultaneous connection attempts
if (connectingRef.current) return;
connectingRef.current = true;
try {
setState('discovering');
setError(undefined);
// Create auth provider if not already created
if (!authProviderRef.current) {
authProviderRef.current = new BrowserOAuthClientProvider(url, {
storageKeyPrefix,
clientName,
clientUri,
callbackUrl,
});
}
// Discover OAuth metadata if not already discovered
if (!metadataRef.current) {
addLog('info', 'Discovering OAuth metadata...');
metadataRef.current = await discoverOAuthMetadata(url);
addLog('debug', `OAuth metadata: ${metadataRef.current ? 'Found' : 'Not available'}`);
}
// Create MCP client
clientRef.current = new Client(
{
name: clientConfig.name || "mcp-react-client",
version: clientConfig.version || "0.1.0",
},
{
capabilities: {
sampling: {},
},
}
);
// Set up auth flow - check if we have tokens
const tokens = await authProviderRef.current.tokens();
// Create SSE transport
setState('connecting');
addLog('info', 'Creating transport...');
const serverUrl = new URL(url);
transportRef.current = new SSEClientTransport(serverUrl, {
authProvider: authProviderRef.current
});
// Set up transport handlers
transportRef.current.onmessage = (message: JSONRPCMessage) => {
addLog('debug', `Received message: ${message.method || message.id}`);
};
transportRef.current.onerror = (err: Error) => {
addLog('error', `Transport error: ${err.message}`);
if (err.message.includes('Unauthorized')) {
setState('authenticating');
handleAuthentication().catch(authErr => {
addLog('error', `Authentication error: ${authErr.message}`);
setState('failed');
setError(`Authentication failed: ${authErr.message}`);
connectingRef.current = false;
});
} else {
setState('failed');
setError(`Connection error: ${err.message}`);
connectingRef.current = false;
}
};
transportRef.current.onclose = () => {
addLog('info', 'Connection closed');
// If we were previously connected, try to reconnect
if (state === 'ready' && autoReconnect) {
const delay = typeof autoReconnect === 'number' ? autoReconnect : 3000;
addLog('info', `Will reconnect in ${delay}ms...`);
setTimeout(() => {
disconnect().then(() => connect());
}, delay);
}
};
// Connect transport
try {
addLog('info', 'Starting transport...');
await transportRef.current.start();
} catch (err) {
addLog('error', `Transport start error: ${err instanceof Error ? err.message : String(err)}`);
if (err instanceof Error && err.message.includes('Unauthorized')) {
setState('authenticating');
// Start authentication process
await handleAuthentication();
// After successful auth, retry connection
return connect();
} else {
setState('failed');
setError(`Connection error: ${err instanceof Error ? err.message : String(err)}`);
connectingRef.current = false;
return;
}
}
// Connect client
try {
addLog('info', 'Connecting client...');
setState('loading');
await clientRef.current.connect(transportRef.current);
addLog('info', 'Client connected');
// Load tools
try {
addLog('info', 'Loading tools...');
const toolsResponse = await clientRef.current.request(
{ method: "tools/list" },
ListToolsResultSchema
);
setTools(toolsResponse.tools);
addLog('info', `Loaded ${toolsResponse.tools.length} tools`);
// Connection completed successfully
setState('ready');
connectingRef.current = false;
} catch (toolErr) {
addLog('error', `Error loading tools: ${toolErr instanceof Error ? toolErr.message : String(toolErr)}`);
// We're still connected, just couldn't load tools
setState('ready');
connectingRef.current = false;
}
} catch (connectErr) {
addLog('error', `Client connect error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`);
setState('failed');
setError(`Connection error: ${connectErr instanceof Error ? connectErr.message : String(connectErr)}`);
connectingRef.current = false;
}
} catch (err) {
addLog('error', `Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
setState('failed');
setError(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
connectingRef.current = false;
}
}, [url, clientName, clientUri, callbackUrl, storageKeyPrefix, clientConfig, debug, autoReconnect, addLog, handleAuthentication, disconnect]);
// Handle authentication flow
handleAuthentication = useCallback(async () => {
if (!authProviderRef.current || !metadataRef.current) {
throw new Error("Auth provider or metadata not available");
}
addLog('info', 'Starting authentication flow...');
// Check if we have client info
let clientInfo = await authProviderRef.current.clientInformation();
if (!clientInfo) {
// Register client dynamically
addLog('info', 'No client information found, registering...');
// Note: In a complete implementation, you'd register the client here
// This would be done server-side in a real application
throw new Error("Dynamic client registration not implemented in this example");
}
// Start authorization flow
addLog('info', 'Starting authorization...');
const {authorizationUrl, codeVerifier} = await startAuthorization(url, {
metadata: metadataRef.current,
clientInformation: clientInfo,
redirectUrl: authProviderRef.current.redirectUrl
});
// Save code verifier
await authProviderRef.current.saveCodeVerifier(codeVerifier);
// Set up listener for post-auth message
const authPromise = new Promise<string>((resolve, reject) => {
const timeoutId = setTimeout(() => {
window.removeEventListener('message', messageHandler);
reject(new Error("Authentication timeout after 5 minutes"));
}, 5 * 60 * 1000);
const messageHandler = (event: MessageEvent) => {
// Verify origin for security
if (event.origin !== window.location.origin) return;
if (event.data && event.data.type === 'mcp_auth_callback' && event.data.code) {
window.removeEventListener('message', messageHandler);
clearTimeout(timeoutId);
resolve(event.data.code);
}
};
window.addEventListener('message', messageHandler);
});
// Redirect to authorization
await authProviderRef.current.redirectToAuthorization(authorizationUrl);
// Wait for auth to complete
addLog('info', 'Waiting for authorization...');
const code = await authPromise;
addLog('info', 'Authorization code received');
return code;
}, [url, addLog]);
// Handle auth completion - this is called when we receive a message from the popup
const handleAuthCompletion = useCallback(async (code: string) => {
if (!authProviderRef.current || !transportRef.current || !metadataRef.current) {
throw new Error("Authentication context not available");
}
try {
addLog('info', 'Finishing authorization...');
await transportRef.current.finishAuth(code);
addLog('info', 'Authorization completed');
// Reconnect with the new auth token
await disconnect();
connect();
} catch (err) {
addLog('error', `Auth completion error: ${err instanceof Error ? err.message : String(err)}`);
setState('failed');
setError(`Authentication failed: ${err instanceof Error ? err.message : String(err)}`);
}
}, [addLog, disconnect, connect]);
// Retry connection
const retry = useCallback(() => {
if (state === 'failed') {
disconnect().then(() => connect());
}
}, [state, disconnect, connect]);
// Set up message listener for auth callback
useEffect(() => {
const messageHandler = (event: MessageEvent) => {
// Verify origin for security
if (event.origin !== window.location.origin) return;
if (event.data && event.data.type === 'mcp_auth_callback' && event.data.code) {
handleAuthCompletion(event.data.code).catch(err => {
addLog('error', `Auth callback error: ${err.message}`);
});
}
};
window.addEventListener('message', messageHandler);
return () => {
window.removeEventListener('message', messageHandler);
};
}, [handleAuthCompletion, addLog]);
// Initial connection and auto-retry
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
connect();
} else if (state === 'failed' && autoRetry) {
const delay = typeof autoRetry === 'number' ? autoRetry : 5000;
const timeoutId = setTimeout(() => {
addLog('info', 'Auto-retrying connection...');
disconnect().then(() => connect());
}, delay);
return () => {
clearTimeout(timeoutId);
};
}
}, [state, autoRetry, connect, disconnect, addLog]);
// Clean up on unmount
useEffect(() => {
return () => {
if (clientRef.current || transportRef.current) {
disconnect();
}
};
}, [disconnect]);
return {
state,
tools,
error,
log,
callTool,
retry,
disconnect
};
} }
/** /**
@ -51,5 +567,127 @@ export function useMcp(
* window to inform any running `useMcp` hooks that the auth flow is complete. * window to inform any running `useMcp` hooks that the auth flow is complete.
*/ */
export async function onMcpAuthorization(query: Record<string, string>) { export async function onMcpAuthorization(query: Record<string, string>) {
// TODO: implement try {
// Extract the authorization code and state
const code = query.code;
const state = query.state;
if (!code) {
throw new Error("No authorization code received");
}
if (!state) {
throw new Error("No state parameter received");
}
// Find the matching auth state in localStorage
const storageKeys = Object.keys(localStorage).filter(key =>
key.includes('_auth_state') && localStorage.getItem(key) === state
);
if (storageKeys.length === 0) {
throw new Error("No matching auth state found in storage");
}
const storageKey = storageKeys[0];
const keyParts = storageKey.split('_');
const serverUrlHash = keyParts[1];
const storageKeyPrefix = keyParts[0];
// Find all related auth data with the same prefix and server hash
const clientInfoKey = `${storageKeyPrefix}_${serverUrlHash}_client_info`;
const codeVerifierKey = `${storageKeyPrefix}_${serverUrlHash}_code_verifier`;
const clientInfoStr = localStorage.getItem(clientInfoKey);
const codeVerifier = localStorage.getItem(codeVerifierKey);
if (!clientInfoStr) {
throw new Error("No client information found in storage");
}
if (!codeVerifier) {
throw new Error("No code verifier found in storage");
}
// Parse client info
const clientInfo = JSON.parse(clientInfoStr) as OAuthClientInformation;
// Find the server URL from other keys in localStorage
const serverUrlKeys = Object.keys(localStorage).filter(key =>
key.startsWith(`${storageKeyPrefix}_server_`) && key.includes(serverUrlHash)
);
let serverUrl: string;
if (serverUrlKeys.length > 0) {
serverUrl = localStorage.getItem(serverUrlKeys[0]) || '';
} else {
// If we can't find the server URL, try to construct it from the current URL
// This is a fallback and may not always work
const currentUrl = new URL(window.location.href);
serverUrl = `${currentUrl.protocol}//${currentUrl.host}`;
}
if (!serverUrl) {
throw new Error("Could not determine server URL");
}
// Exchange the code for tokens
const metadata = await discoverOAuthMetadata(serverUrl);
const tokens = await exchangeAuthorization(serverUrl, {
metadata,
clientInformation: clientInfo,
authorizationCode: code,
codeVerifier,
});
// Save the tokens
const tokensKey = `${storageKeyPrefix}_${serverUrlHash}_tokens`;
localStorage.setItem(tokensKey, JSON.stringify(tokens));
// Post message back to the parent window
if (window.opener && !window.opener.closed) {
window.opener.postMessage({
type: 'mcp_auth_callback',
code
}, window.location.origin);
// Close the popup
window.close();
} else {
// If no parent window, we're in a redirect flow
// Redirect back to the main page
window.location.href = '/';
}
return { success: true };
} catch (error) {
console.error('Error in MCP authorization:', error);
// Create a readable error message for display
const errorMessage = error instanceof Error ? error.message : String(error);
// If the popup is still open, show the error
const errorHtml = `
<html>
<head>
<title>Authentication Error</title>
<style>
body { font-family: sans-serif; padding: 2rem; line-height: 1.5; }
.error { color: #e53e3e; background: #fed7d7; padding: 1rem; border-radius: 0.25rem; }
</style>
</head>
<body>
<h1>Authentication Error</h1>
<div class="error">
<p>${errorMessage}</p>
</div>
<p>You can close this window and try again.</p>
</body>
</html>
`;
document.body.innerHTML = errorHtml;
return { success: false, error: errorMessage };
}
} }

View file

@ -7,8 +7,9 @@
"rootDir": "./src", "rootDir": "./src",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["ES2022"],
"types": ["node"], "lib": ["ES2022", "DOM"],
"types": ["node", "react"],
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },

View file

@ -6,5 +6,5 @@ export default defineConfig({
dts: true, dts: true,
clean: true, clean: true,
outDir: 'dist', outDir: 'dist',
// external: ['typescript'], external: ['react'],
}) })