diff --git a/app/actions/create-token.ts b/app/actions/create-token.ts deleted file mode 100644 index 6c3548d..0000000 --- a/app/actions/create-token.ts +++ /dev/null @@ -1,92 +0,0 @@ -"use server"; - -import Smithery from "@smithery/api"; -import type { CreateTokenResponse } from "@smithery/api/resources/tokens.mjs"; - -const SMITHERY_API_KEY = process.env.SMITHERY_API_KEY; -const SMITHERY_API_URL = process.env.NEXT_PUBLIC_SMITHERY_API_URL; -const DEFAULT_NAMESPACE = "sandbox"; -const DEFAULT_TTL = "1h"; - -interface CreateSandboxTokenParams { - userId: string; - namespace?: string; - ttl?: string | number; -} - -type CreateSandboxTokenResult = - | { - success: true; - token: CreateTokenResponse; - } - | { - success: false; - error: string; - code: "NO_API_KEY" | "NAMESPACE_NOT_FOUND" | "TOKEN_CREATION_FAILED"; - }; - -export async function createSandboxToken( - params: CreateSandboxTokenParams, -): Promise { - const { userId, namespace = DEFAULT_NAMESPACE, ttl = DEFAULT_TTL } = params; - - if (!SMITHERY_API_KEY) { - return { - success: false, - error: "Server is not configured with Smithery API key", - code: "NO_API_KEY", - }; - } - - const client = new Smithery({ - apiKey: SMITHERY_API_KEY, - baseURL: SMITHERY_API_URL, - }); - - try { - // Verify namespace exists - const namespacesResponse = await client.namespaces.list(); - const namespaceExists = namespacesResponse.namespaces.some( - (ns) => ns.name === namespace, - ); - - if (!namespaceExists) { - return { - success: false, - error: `Namespace "${namespace}" does not exist. Please create it first.`, - code: "NAMESPACE_NOT_FOUND", - }; - } - - // Create scoped token with user isolation via metadata - const tokenResponse = await client.tokens.create({ - policy: [ - { - namespaces: [namespace], - operations: ["read", "write"], - resources: ["connections"], - metadata: { user_id: userId }, - ttl, - }, - { - namespaces: [namespace], - operations: ["read"], - resources: ["servers", "skills"], - ttl, - }, - ], - }); - - return { - success: true, - token: tokenResponse, - }; - } catch (error) { - console.error("Failed to create sandbox token:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - code: "TOKEN_CREATION_FAILED", - }; - } -} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 33fc955..3a205f6 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,4 +1,4 @@ -import type { Connection } from "@smithery/api/resources/experimental/connect/connections.mjs"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import { createAgentUIStreamResponse, ToolLoopAgent, diff --git a/app/api/tool-search/route.ts b/app/api/tool-search/route.ts index 60fe63e..daf80bb 100644 --- a/app/api/tool-search/route.ts +++ b/app/api/tool-search/route.ts @@ -2,7 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Smithery } from "@smithery/api"; import { createConnection } from "@smithery/api/lib/mcp-transport.mjs"; -import type { Connection } from "@smithery/api/resources/experimental/connect/connections"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import { Index } from "flexsearch"; import { estimateTokenCount } from "tokenx"; import type { diff --git a/app/docs/docs-layout-client.tsx b/app/docs/docs-layout-client.tsx index 554fc17..e3b95a9 100644 --- a/app/docs/docs-layout-client.tsx +++ b/app/docs/docs-layout-client.tsx @@ -4,9 +4,17 @@ import { Suspense } from "react"; import { SharedSidebar } from "@/components/shared-sidebar"; import { SmitheryProvider } from "@/registry/new-york/smithery/smithery-provider"; -export function DocsLayoutClient({ children }: { children: React.ReactNode }) { +interface DocsLayoutClientProps { + children: React.ReactNode; + smitheryApiKey?: string; +} + +export function DocsLayoutClient({ + children, + smitheryApiKey, +}: DocsLayoutClientProps) { return ( - + {children}; + return ( + + {children} + + ); } diff --git a/app/docs/tokens/page.mdx b/app/docs/tokens/page.mdx index 8a7bfbe..49eb8ac 100644 --- a/app/docs/tokens/page.mdx +++ b/app/docs/tokens/page.mdx @@ -1,8 +1,8 @@ # Tokens -The Tokens component provides a token selector and manager for Smithery API authentication. It allows users to manage multiple API tokens, switch between them, and create new ones. +The Tokens component displays the configured Smithery API key and namespace settings. It reads the API key from the server and renders token details client-side. -The preview for this component is displayed in the header above - click "Modify" to see the token management dialog. +The preview for this component is displayed in the header above. Click "Settings" to manage namespace selection and creation. ## Installation @@ -13,35 +13,18 @@ The preview for this component is displayed in the header above - click "Modify" ```tsx import { Tokens } from "@/registry/new-york/smithery/tokens" -// You need an initial token response from your server -const initialTokenResponse = { - token: "your-api-token", - expiresAt: "2024-12-31T23:59:59Z" // or "never" -} - export function MyApp() { - return ( - { - // Optional: fetch a new token from your server - const response = await fetch("/api/create-token") - return response.json() - }} - /> - ) + return } ``` ## Props -- `initialTokenResponse` (required): The initial token to display, with `token` and `expiresAt` fields -- `onCreateToken` (optional): Async function to create a new token. If not provided, the "Create New Token" button won't appear +- This component has no direct props. It consumes `useSmitheryContext` from `SmitheryProvider`. ## Features -- Display the currently selected token (masked for security) -- Switch between multiple tokens via a dropdown -- Remove expired or unused tokens -- Create new tokens via the optional callback -- Persists tokens to localStorage using jotai atoms +- Display the configured API key in masked form +- Show the currently selected namespace +- Switch namespaces from a dropdown +- Create new namespaces directly from the settings dialog diff --git a/app/page.tsx b/app/page.tsx index b7913e1..9076cb7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ import { HomePage } from "@/components/home"; export default function Home() { - return ; + return ; } diff --git a/components/chat-block.tsx b/components/chat-block.tsx index bc58611..11c1dc0 100644 --- a/components/chat-block.tsx +++ b/components/chat-block.tsx @@ -1,6 +1,6 @@ "use client"; import { useChat } from "@ai-sdk/react"; -import type { Connection } from "@smithery/api/resources/experimental/connect/connections.mjs"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, diff --git a/components/docs/component-preview.tsx b/components/docs/component-preview.tsx index 9b470ef..3454256 100644 --- a/components/docs/component-preview.tsx +++ b/components/docs/component-preview.tsx @@ -3,7 +3,7 @@ import { createMCPClient } from "@ai-sdk/mcp"; import Smithery from "@smithery/api"; import { createConnection } from "@smithery/api/mcp"; -import type { Connection } from "@smithery/api/resources/experimental/connect/connections.mjs"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import { useQuery } from "@tanstack/react-query"; import type { Tool, ToolExecutionOptions } from "ai"; import { useAtomValue } from "jotai"; @@ -40,8 +40,7 @@ function useConnections( if (!token) throw new Error("Token required"); if (!namespace) throw new Error("Namespace required"); const client = getSmitheryClient(token); - const { connections } = - await client.experimental.connect.connections.list(namespace); + const { connections } = await client.connections.list(namespace); return { connections, namespace }; }, enabled: !!token && !!namespace, diff --git a/components/docs/previews.tsx b/components/docs/previews.tsx index e2f2d1a..26713be 100644 --- a/components/docs/previews.tsx +++ b/components/docs/previews.tsx @@ -3,7 +3,7 @@ import { createMCPClient } from "@ai-sdk/mcp"; import Smithery from "@smithery/api"; import { createConnection } from "@smithery/api/mcp"; -import type { Connection } from "@smithery/api/resources/experimental/connect/connections.mjs"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import { useQuery } from "@tanstack/react-query"; import type { ToolExecutionOptions } from "ai"; import { AlertCircle } from "lucide-react"; @@ -65,8 +65,7 @@ function useConnections(token: string, namespace: string) { queryKey: ["connections", token, namespace], queryFn: async () => { const client = getSmitheryClient(token); - const { connections } = - await client.experimental.connect.connections.list(namespace); + const { connections } = await client.connections.list(namespace); return { connections, namespace }; }, }); diff --git a/components/home.tsx b/components/home.tsx index cfba3c4..ea2a222 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -4,10 +4,14 @@ import { Suspense } from "react"; import { SmitheryProvider } from "@/registry/new-york/smithery/smithery-provider"; import { RegistryBrowser } from "./registry-browser"; -export function HomePage() { +interface HomePageProps { + smitheryApiKey?: string; +} + +export function HomePage({ smitheryApiKey }: HomePageProps) { return ( - + diff --git a/hooks/use-smithery.ts b/hooks/use-smithery.ts index 239457d..e6394bc 100644 --- a/hooks/use-smithery.ts +++ b/hooks/use-smithery.ts @@ -4,33 +4,19 @@ import Smithery from "@smithery/api"; import type { CreateTokenResponse } from "@smithery/api/resources/tokens.mjs"; import { useQuery } from "@tanstack/react-query"; import { atom, useAtom } from "jotai"; -import { atomWithStorage } from "jotai/utils"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { createSandboxToken } from "@/app/actions/create-token"; -import { filterExpiredTokens, isTokenExpired } from "@/lib/utils"; +import { useCallback, useEffect, useMemo, useState } from "react"; -// Jotai atoms for state management -export const tokensCreatedAtom = atomWithStorage( - "tokensCreated", - [], -); export const selectedTokenAtom = atom(null); export const selectedNamespaceAtom = atom(null); -export const userIdAtom = atomWithStorage( - "smithery_user_id", - null, -); -export const sandboxModeAtom = atom(false); export interface UseSmitheryOptions { baseURL?: string; + smitheryApiKey?: string; } export interface UseSmitheryReturn { - createToken(): Promise; createNamespace(name: string): Promise; token: string; - tokenExpiresAt: string; namespace: string; setNamespace(namespace: string): void; namespaces: string[]; @@ -38,70 +24,16 @@ export interface UseSmitheryReturn { error?: Error; connected: boolean; client: Smithery; - sandboxMode: boolean; -} - -export { SmitheryConnectionError }; - -class SmitheryConnectionError extends Error { - constructor( - message: string, - public readonly isServiceUnavailable: boolean, - ) { - super(message); - this.name = "SmitheryConnectionError"; - } -} - -async function fetchTokenFromWhoami(): Promise { - let response: Response; - try { - response = await fetch("http://localhost:4260/whoami"); - } catch { - throw new SmitheryConnectionError( - "Unable to connect to Smithery service. Make sure the Smithery agent is running on localhost:4260.", - true, - ); - } - - if (!response.ok) { - throw new SmitheryConnectionError( - `Smithery service returned an error: ${response.status} ${response.statusText}`, - false, - ); - } - - const data = await response.json(); - if (!data.SMITHERY_API_KEY) { - throw new SmitheryConnectionError( - "Smithery service did not return an API key. Please check your Smithery configuration.", - false, - ); - } - - return { - token: data.SMITHERY_API_KEY, - expiresAt: data.expiresAt ?? "never", - }; } export function useSmithery( options: UseSmitheryOptions = {}, ): UseSmitheryReturn { - const { baseURL } = options; + const { baseURL, smitheryApiKey } = options; - // Token state - const [tokensCreated, setTokensCreated] = useAtom(tokensCreatedAtom); const [selectedToken, setSelectedToken] = useAtom(selectedTokenAtom); const [tokenLoading, setTokenLoading] = useState(true); const [tokenError, setTokenError] = useState(); - const [hydrated, setHydrated] = useState(false); - const fetchStarted = useRef(false); - - // User ID for sandbox mode isolation - const [userId, setUserId] = useAtom(userIdAtom); - const [sandboxMode, setSandboxMode] = useAtom(sandboxModeAtom); - const userIdRef = useRef(null); // Namespace state const [selectedNamespace, setSelectedNamespace] = useAtom( @@ -116,15 +48,10 @@ export function useSmithery( }); }, [selectedToken?.token, baseURL]); - // Fetch namespaces (only when we have a valid token and NOT in sandbox mode) - // Sandbox mode tokens don't have namespaces:read permission + // Fetch namespaces when we have a valid token const namespacesQuery = useQuery({ - queryKey: ["namespaces", selectedToken?.token, sandboxMode], + queryKey: ["namespaces", selectedToken?.token], queryFn: async () => { - // In sandbox mode, we only have access to "sandbox" namespace - if (sandboxMode) { - return ["sandbox"]; - } const response = await client.namespaces.list(); return response.namespaces.map((ns) => ns.name); }, @@ -132,175 +59,36 @@ export function useSmithery( staleTime: 5 * 60 * 1000, // 5 minutes }); - // Wait for atom to hydrate - useEffect(() => { - setHydrated(true); - }, []); - - // Generate user ID if not exists (for sandbox mode isolation) - useEffect(() => { - if (!hydrated) return; - if (!userId) { - const newUserId = crypto.randomUUID(); - setUserId(newUserId); - userIdRef.current = newUserId; - } else { - userIdRef.current = userId; - } - }, [hydrated, userId, setUserId]); - - // Filter expired tokens after hydration - useEffect(() => { - if (!hydrated) return; - setTokensCreated((current) => { - const validTokens = filterExpiredTokens(current); - if (validTokens.length !== current.length) { - return validTokens; - } - return current; - }); - }, [hydrated, setTokensCreated]); - - // Fetch token after hydration - useEffect(() => { - if (!hydrated) return; - if (fetchStarted.current) return; - fetchStarted.current = true; - - async function fetchToken() { - try { - const tokenResponse = await fetchTokenFromWhoami(); - - if (tokenResponse) { - setTokensCreated((current) => { - const alreadyExists = current.some( - (t) => t.token === tokenResponse.token, - ); - if (alreadyExists) return current; - return [tokenResponse, ...current]; - }); - setSelectedToken(tokenResponse); - setSandboxMode(false); - } - } catch (err) { - // Fallback to server-side token creation when whoami is unavailable - if ( - err instanceof SmitheryConnectionError && - err.isServiceUnavailable - ) { - const currentUserId = userIdRef.current; - if (!currentUserId) { - // Wait for userId to be generated, retry later - fetchStarted.current = false; - setTokenLoading(false); - return; - } - - try { - const result = await createSandboxToken({ userId: currentUserId }); - if (result.success) { - setTokensCreated((current) => { - const alreadyExists = current.some( - (t) => t.token === result.token.token, - ); - if (alreadyExists) return current; - return [result.token, ...current]; - }); - setSelectedToken(result.token); - setSandboxMode(true); - setSelectedNamespace("sandbox"); - } else { - setTokenError(new Error(result.error)); - } - } catch (sandboxErr) { - setTokenError( - sandboxErr instanceof Error - ? sandboxErr - : new Error(String(sandboxErr)), - ); - } - } else { - setTokenError(err instanceof Error ? err : new Error(String(err))); - } - } finally { - setTokenLoading(false); - } - } - - fetchToken(); - }, [ - hydrated, - setSelectedToken, - setTokensCreated, - setSandboxMode, - setSelectedNamespace, - ]); - - // Select first valid token if none selected or current selection is expired + // Initialize selected token from the server-provided API key useEffect(() => { - const needsNewSelection = !selectedToken || isTokenExpired(selectedToken); - if (needsNewSelection && tokensCreated.length > 0) { - const validToken = tokensCreated.find((t) => !isTokenExpired(t)); - if (validToken) setSelectedToken(validToken); + if (!smitheryApiKey) { + setSelectedToken(null); + setTokenError( + new Error( + "Smithery API key is not configured. Set SMITHERY_API_KEY on the server.", + ), + ); + setTokenLoading(false); + return; } - }, [selectedToken, setSelectedToken, tokensCreated]); - - // Poll whoami when in sandbox mode to check if service comes back online - useEffect(() => { - if (!sandboxMode || !hydrated) return; - - const POLL_INTERVAL_MS = 3000; - - const intervalId = setInterval(async () => { - try { - const tokenResponse = await fetchTokenFromWhoami(); - if (tokenResponse) { - setTokensCreated((current) => { - const alreadyExists = current.some( - (t) => t.token === tokenResponse.token, - ); - if (alreadyExists) return current; - return [tokenResponse, ...current]; - }); - setSelectedToken(tokenResponse); - setSandboxMode(false); - } - } catch { - // Service still unavailable, continue polling - } - }, POLL_INTERVAL_MS); - - return () => clearInterval(intervalId); - }, [ - sandboxMode, - hydrated, - setTokensCreated, - setSelectedToken, - setSandboxMode, - ]); + const tokenResponse: CreateTokenResponse = { + token: smitheryApiKey, + expiresAt: "never", + }; + setSelectedToken(tokenResponse); + setTokenError(undefined); + setTokenLoading(false); + }, [smitheryApiKey, setSelectedToken]); // Auto-select first namespace if none selected useEffect(() => { - if ( - !selectedNamespace && - namespacesQuery.data && - namespacesQuery.data.length > 0 - ) { - setSelectedNamespace(namespacesQuery.data[0]); + const namespaces = namespacesQuery.data; + if (!namespaces || namespaces.length === 0) return; + if (!selectedNamespace || !namespaces.includes(selectedNamespace)) { + setSelectedNamespace(namespaces[0]); } }, [selectedNamespace, namespacesQuery.data, setSelectedNamespace]); - // Create token function (refreshes from /whoami) - const createToken = useCallback(async (): Promise => { - const tokenResponse = await fetchTokenFromWhoami(); - if (tokenResponse) { - setTokensCreated((prev) => [...prev, tokenResponse]); - setSelectedToken(tokenResponse); - return tokenResponse.token; - } - throw new Error("Failed to fetch token from /whoami"); - }, [setTokensCreated, setSelectedToken]); - // Set namespace function const setNamespace = useCallback( (namespace: string) => { @@ -337,16 +125,19 @@ export function useSmithery( tokenLoading || (!!selectedToken?.token && namespacesQuery.isPending); // Combined error state - const error = tokenError ?? namespacesQuery.error ?? undefined; + const queryError = namespacesQuery.error + ? namespacesQuery.error instanceof Error + ? namespacesQuery.error + : new Error(String(namespacesQuery.error)) + : undefined; + const error = tokenError ?? queryError; // Connected if we have a token and no connection error const connected = !!selectedToken?.token && !tokenError; return { - createToken, createNamespace, token: selectedToken?.token ?? "", - tokenExpiresAt: selectedToken?.expiresAt ?? "", namespace: selectedNamespace ?? "", setNamespace, namespaces: namespacesQuery.data ?? [], @@ -354,6 +145,5 @@ export function useSmithery( error, connected, client, - sandboxMode, }; } diff --git a/registry/new-york/smithery/connections.tsx b/registry/new-york/smithery/connections.tsx index cd6c6f7..d0b6690 100644 --- a/registry/new-york/smithery/connections.tsx +++ b/registry/new-york/smithery/connections.tsx @@ -3,7 +3,7 @@ import { createMCPClient } from "@ai-sdk/mcp"; import Smithery from "@smithery/api"; import { createConnection } from "@smithery/api/mcp"; -import type { Connection } from "@smithery/api/resources/experimental/connect/connections.mjs"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { ToolExecutionOptions } from "ai"; import { Plus, RefreshCw, Trash2, X } from "lucide-react"; @@ -46,12 +46,9 @@ const ConnectionCardInner = ({ const deleteMutation = useMutation({ mutationFn: async () => { const client = getSmitheryClient(token); - await client.experimental.connect.connections.delete( - connection.connectionId, - { - namespace: namespace, - }, - ); + await client.connections.delete(connection.connectionId, { + namespace: namespace, + }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["connections"] }); @@ -74,7 +71,9 @@ const ConnectionCardInner = ({ connection.name} {connection.connectionId && ( - {"•".repeat(Math.min(connection.connectionId.length - 10, 4))} + {"•".repeat( + Math.max(0, Math.min(connection.connectionId.length - 10, 4)), + )} {connection.connectionId.slice(-10)} )} @@ -134,8 +133,7 @@ const ConnectionsListInner = ({ queryKey: ["connections", token, namespace], queryFn: async () => { const client = getSmitheryClient(token); - const { connections } = - await client.experimental.connect.connections.list(namespace); + const { connections } = await client.connections.list(namespace); return { connections, namespace }; }, enabled: !!token && !!namespace, @@ -237,12 +235,9 @@ const ActiveConnection = ({ connectionId }: { connectionId: string }) => { queryKey: ["connection", connectionId, token, namespace], queryFn: async () => { const client = getSmitheryClient(token); - const data = await client.experimental.connect.connections.get( - connectionId, - { - namespace, - }, - ); + const data = await client.connections.get(connectionId, { + namespace, + }); return { namespace, ...data }; }, enabled: !!token && !!namespace && !!connectionId, diff --git a/registry/new-york/smithery/server-search.tsx b/registry/new-york/smithery/server-search.tsx index 83db3b2..2c41d33 100644 --- a/registry/new-york/smithery/server-search.tsx +++ b/registry/new-york/smithery/server-search.tsx @@ -6,7 +6,7 @@ import { createConnection, SmitheryAuthorizationError, } from "@smithery/api/mcp"; -import type { Connection } from "@smithery/api/resources/experimental/connect/connections.mjs"; +import type { Connection } from "@smithery/api/resources/connections/connections.mjs"; import type { ServerListResponse } from "@smithery/api/resources/servers/servers.mjs"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { @@ -432,12 +432,9 @@ async function checkConnectionStatus( const tools = await mcpClient.tools(); console.log("tools list", tools); - const connection = await client.experimental.connect.connections.get( - connectionId, - { - namespace: namespace, - }, - ); + const connection = await client.connections.get(connectionId, { + namespace: namespace, + }); return { status: "connected", @@ -477,12 +474,9 @@ async function getOrCreateConnection( onExistingConnection: OnExistingConnectionMode, ): Promise { // Check for existing connection by name - const existingList = await client.experimental.connect.connections.list( - namespace, - { - name, - }, - ); + const existingList = await client.connections.list(namespace, { + name, + }); const existing = existingList.connections[0]; if (existing) { @@ -508,13 +502,10 @@ async function getOrCreateConnection( // Create new connection (API auto-generates unique ID) console.log("creating connection", name); - const connection = await client.experimental.connect.connections.create( - namespace, - { - mcpUrl, - name, - }, - ); + const connection = await client.connections.create(namespace, { + mcpUrl, + name, + }); console.log("connection", connection); if (connection.status?.state === "auth_required") { diff --git a/registry/new-york/smithery/smithery-provider.tsx b/registry/new-york/smithery/smithery-provider.tsx index bffa150..e31124e 100644 --- a/registry/new-york/smithery/smithery-provider.tsx +++ b/registry/new-york/smithery/smithery-provider.tsx @@ -1,12 +1,7 @@ "use client"; -import { createContext, type ReactNode, useContext, useState } from "react"; +import { createContext, type ReactNode, useContext } from "react"; import { - CodeBlock, - CodeBlockCopyButton, -} from "@/components/ai-elements/code-block"; -import { - SmitheryConnectionError, type UseSmitheryOptions, type UseSmitheryReturn, useSmithery, @@ -21,48 +16,26 @@ interface SmitheryProviderProps extends UseSmitheryOptions { } function ConnectionError({ error }: { error: Error }) { - const isServiceUnavailable = - error instanceof SmitheryConnectionError && error.isServiceUnavailable; - const [_isCopied, setIsCopied] = useState(false); - const command = "npx @smithery/cli@latest whoami --server"; - - const _copyToClipboard = async () => { - if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { - return; - } - - try { - await navigator.clipboard.writeText(command); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - } catch { - // Ignore errors - } - }; - return (
- {isServiceUnavailable - ? "Unable to connect to Smithery" - : "Connection Error"} + Connection Error

{error.message}

- {isServiceUnavailable && ( - - - - )}
); } -export function SmitheryProvider({ children, baseURL }: SmitheryProviderProps) { - const value = useSmithery({ baseURL }); +export function SmitheryProvider({ + children, + baseURL, + smitheryApiKey, +}: SmitheryProviderProps) { + const value = useSmithery({ baseURL, smitheryApiKey }); if (value.error && !value.loading) { return ( diff --git a/registry/new-york/smithery/tokens.tsx b/registry/new-york/smithery/tokens.tsx index e7f2313..83072d8 100644 --- a/registry/new-york/smithery/tokens.tsx +++ b/registry/new-york/smithery/tokens.tsx @@ -1,12 +1,7 @@ "use client"; -import { useAtom } from "jotai"; -import { Loader2, Plus, Trash2 } from "lucide-react"; +import { Loader2, Plus } from "lucide-react"; import { useState } from "react"; -import { - CodeBlock, - CodeBlockCopyButton, -} from "@/components/ai-elements/code-block"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -23,61 +18,31 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { selectedTokenAtom, tokensCreatedAtom } from "@/hooks/use-smithery"; import { useSmitheryContext } from "@/registry/new-york/smithery/smithery-provider"; -function formatTimeRemaining(expiresAt: string): string { - if (!expiresAt || expiresAt === "never") return "1 hour"; - const expiresDate = new Date(expiresAt); - const now = new Date(); - const diffMs = expiresDate.getTime() - now.getTime(); - if (diffMs <= 0) return "expired"; - const diffMins = Math.floor(diffMs / 60000); - if (diffMins < 60) { - return `${diffMins} minute${diffMins !== 1 ? "s" : ""}`; - } - const diffHours = Math.floor(diffMins / 60); - return `${diffHours} hour${diffHours !== 1 ? "s" : ""}`; +function getTokenLabel(token: string): string { + const tokenType = token.startsWith("v4.public") + ? "Service Token" + : "Root API Key"; + const suffix = token.length >= 4 ? token.slice(-4) : token; + return `${tokenType} *****${suffix}`; } export function Tokens() { - const [tokensCreated, setTokensCreated] = useAtom(tokensCreatedAtom); - const [selectedToken, setSelectedToken] = useAtom(selectedTokenAtom); const [isOpen, setIsOpen] = useState(false); - const [isCreatingToken, setIsCreatingToken] = useState(false); const [isCreatingNamespace, setIsCreatingNamespace] = useState(false); const [newNamespaceName, setNewNamespaceName] = useState(""); const [showNamespaceInput, setShowNamespaceInput] = useState(false); + const [namespaceError, setNamespaceError] = useState(null); const { - createToken, createNamespace, loading, namespace, namespaces, setNamespace, - sandboxMode, - tokenExpiresAt, + token, } = useSmitheryContext(); - const [namespaceError, setNamespaceError] = useState(null); - - const handleRemoveToken = () => { - if (!selectedToken) return; - - setTokensCreated((prev) => - prev.filter((token) => token.token !== selectedToken.token), - ); - setSelectedToken(null); - }; - - const handleCreateToken = async () => { - setIsCreatingToken(true); - try { - await createToken(); - } finally { - setIsCreatingToken(false); - } - }; const handleCreateNamespace = async () => { if (!newNamespaceName.trim()) return; @@ -105,26 +70,15 @@ export function Tokens() { ); } - if (!selectedToken) return null; + if (!token) return null; return (
- - {selectedToken.token.startsWith("v4.public") - ? "Service Token" - : "Root API Key"}{" "} - *****{selectedToken.token.slice(-4)} - - {sandboxMode ? ( - - Sandbox Mode + {getTokenLabel(token)} + {namespace && ( + + {namespace} - ) : ( - namespace && ( - - {namespace} - - ) )} @@ -137,184 +91,83 @@ export function Tokens() { Settings
- {/* Namespace Section (or Sandbox Mode info) */} - {sandboxMode ? ( -
-
- - Sandbox Mode - - - Token expires in {formatTimeRemaining(tokenExpiresAt)} - -
-

- You're using a temporary token in sandbox mode. Your - connections are private to this browser, but they will expire - in 1 hour. -

-
- - Persist Connections with a Smithery Account - -

- To save your work and use Smithery across devices, create a - free account and switch to Persistent Mode by running: -

- - - - -
-
- ) : ( -
-
Namespace
- {namespaces.length > 0 && ( - - )} - {namespaceError && ( -
- {namespaceError} -
- )} - {showNamespaceInput ? ( -
- setNewNamespaceName(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleCreateNamespace(); - if (e.key === "Escape") { - setShowNamespaceInput(false); - setNewNamespaceName(""); - } - }} - disabled={isCreatingNamespace} - /> - - -
- ) : ( - - )} -
- )} - - {/* Token Section */}
-
Token
- {tokensCreated.length > 1 && ( - - + - {tokensCreated.map((token) => ( - - {token.token.startsWith("v4.public") - ? "Service Token" - : "Root API Key"} - : *****{token.token.slice(-4)} + {namespaces.map((ns) => ( + + {ns} ))} )} - -
-
-
-
- {selectedToken.token.startsWith("v4.public") - ? "Service Token" - : "Root API Key"} - : *****{selectedToken.token.slice(-4)} -
- {selectedToken.expiresAt !== "never" && ( -
- Expires:{" "} - {new Date(selectedToken.expiresAt).toLocaleString()} -
+ {namespaceError && ( +
+ {namespaceError} +
+ )} + {showNamespaceInput ? ( +
+ setNewNamespaceName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleCreateNamespace(); + if (e.key === "Escape") { + setShowNamespaceInput(false); + setNewNamespaceName(""); + } + }} + disabled={isCreatingNamespace} + /> +
+
-
+ ) : ( + + )} +
- +
+
API Key
+
+
{getTokenLabel(token)}
+