Skip to main content
Glama
tools.ts11.3 kB
import { toToolId } from "@/utils"; import { AppConfig, ToolExtension } from "@mcpx/shared-model"; import { TargetServerTool } from "@mcpx/shared-model/api"; import { create } from "zustand"; import { useShallow } from "zustand/react/shallow"; import { SocketStore, socketStore } from "./socket"; export interface ServerTool { description?: string; id: string; inputSchema?: TargetServerTool["inputSchema"]; name: string; serviceName: string; } export interface CustomTool { description?: { action: "append" | "rewrite"; text: string; }; name: string; originalName?: string; originalTool: ServerTool; overrideParams: ToolExtension["overrideParams"]; } export interface ToolsState { customTools: CustomTool[]; tools: ServerTool[]; } export interface ToolsActions { createCustomTool: (tool: CustomTool) => Promise<AppConfig>; deleteCustomTool: (tool: CustomTool) => Promise<AppConfig>; init: (socketStoreState: SocketStore) => void; setTools: ( update: | ToolsState["tools"] | ((tools: ToolsState["tools"]) => ToolsState["tools"]), ) => void; updateCustomTool: (tool: CustomTool) => AppConfig; } export type ToolsStore = ToolsState & ToolsActions; const initialState: ToolsState = { customTools: [], tools: [] }; // Helper to wait for appConfig to be available async function waitForAppConfig( maxAttempts = 20, delay = 50, ): Promise<AppConfig | null> { for (let i = 0; i < maxAttempts; i++) { const { appConfig } = socketStore.getState(); if (appConfig) { return appConfig; } // Wait using a Promise await new Promise((resolve) => setTimeout(resolve, delay)); } // Final attempt const finalAppConfig = socketStore.getState().appConfig; if (!finalAppConfig) { console.error("[Tools] appConfig still not available after max attempts"); } return finalAppConfig; } // Extended type for tool extensions with childTools type ToolExtensionWithChildren = Partial<ToolExtension> & { childTools?: ToolExtension[]; }; function createCustomToolImpl( payload: CustomTool, appConfig: AppConfig, ): AppConfig { const newToolExtension: ToolExtension = { description: payload.description, name: payload.name, overrideParams: Object.fromEntries( Object.entries(payload.overrideParams).filter( ([, value]) => value !== undefined, ), ) as ToolExtension["overrideParams"], }; // Build the tool extensions structure const toolExtensions = { ...(appConfig.toolExtensions?.services || {}), } as Record<string, Record<string, ToolExtensionWithChildren>>; // Ensure the service exists if (!toolExtensions[payload.originalTool.serviceName]) { toolExtensions[payload.originalTool.serviceName] = {}; } // Ensure the original tool exists if ( !toolExtensions[payload.originalTool.serviceName][payload.originalTool.name] ) { toolExtensions[payload.originalTool.serviceName][ payload.originalTool.name ] = { childTools: [], } as ToolExtensionWithChildren; } // Add the new custom tool to the child tools const toolExt = toolExtensions[payload.originalTool.serviceName][payload.originalTool.name]; if (!toolExt.childTools) { toolExt.childTools = []; } toolExt.childTools.push(newToolExtension); const updates: AppConfig = { ...appConfig, toolExtensions: { services: toolExtensions as AppConfig["toolExtensions"]["services"], }, }; return updates; } function deleteCustomToolImpl( tool: CustomTool, appConfig: AppConfig, customTools: CustomTool[], ): AppConfig { const newCustomTools = customTools.filter( (t) => !(t.originalTool.id === tool.originalTool.id && t.name === tool.name), ); type ServiceToolsMap = Record< string, Record<string, ToolExtensionWithChildren> >; const services: ServiceToolsMap = Object.fromEntries( Object.entries( (appConfig.toolExtensions?.services || {}) as ServiceToolsMap, ) .filter(([serviceName]) => newCustomTools.some((t) => t.originalTool.serviceName === serviceName), ) .map(([serviceName, serviceTools]) => [ serviceName, Object.fromEntries( Object.entries( serviceTools as Record<string, ToolExtensionWithChildren>, ).filter(([toolName]) => newCustomTools.some( (t) => t.originalTool.name === toolName && t.originalTool.serviceName === serviceName, ), ), ), ]), ); const updates: AppConfig = { ...appConfig, toolExtensions: { services: newCustomTools.reduce((acc: ServiceToolsMap, t) => { const serviceTools = acc[t.originalTool.serviceName] || {}; const existingTool: ToolExtensionWithChildren = serviceTools[ t.originalTool.name ] || { childTools: [], }; existingTool.childTools = (existingTool.childTools || []).filter( (ct: ToolExtension) => !( t.originalTool.id === tool.originalTool.id && ct.name === tool.name ), ); if (existingTool.childTools.length > 0) { serviceTools[t.originalTool.name] = existingTool; } else { delete serviceTools[t.originalTool.name]; } acc[t.originalTool.serviceName] = serviceTools; return acc; }, services) as AppConfig["toolExtensions"]["services"], }, }; return updates; } const toolsStore = create<ToolsStore>((set, get) => ({ ...initialState, createCustomTool: async (payload) => { const appConfig = await waitForAppConfig(); if (!appConfig) { throw new Error("App config is not available."); } return createCustomToolImpl(payload, appConfig); }, deleteCustomTool: async (tool) => { const appConfig = await waitForAppConfig(); if (!appConfig) { throw new Error("App config is not available."); } const { customTools } = get(); return deleteCustomToolImpl(tool, appConfig, customTools); }, init: (socketStoreState: SocketStore) => { if (!socketStoreState.systemState || !socketStoreState.appConfig) { return; } const { systemState, appConfig } = socketStoreState; const customTools: CustomTool[] = []; for (const [serviceName, serviceTools] of Object.entries( appConfig.toolExtensions?.services || {}, )) { // TODO: Maybe populate custom tools regardless of the service tools? for (const [originalToolName, { childTools }] of Object.entries( serviceTools, )) { const originalTool = systemState.targetServers_new .find((server) => server.name === serviceName) ?.originalTools.find((tool) => tool.name === originalToolName); if (!originalTool) { continue; } const toolsForService = childTools.map( (toolExtension: ToolExtension) => { const parameterDescriptions = Object.fromEntries( Object.entries(toolExtension.overrideParams || {}) .map(([name, param]) => { const descObject = param?.description; if (!descObject || typeof descObject.text !== "string") { return [name, undefined]; } const text = descObject.text.trim(); if (!text) { return [name, undefined]; } return [name, text]; }) .filter(([, text]) => text !== undefined), ); return { description: toolExtension.description, id: toToolId(serviceName, toolExtension.name), name: toolExtension.name, originalTool: { description: originalTool.description || "", id: toToolId(serviceName, originalToolName), inputSchema: originalTool.inputSchema, name: originalToolName, serviceName, }, overrideParams: toolExtension.overrideParams, parameterDescriptions, }; }, ); customTools.push(...toolsForService); } } const tools: ToolsState["tools"] = []; for (const targetServer of systemState.targetServers_new) { const serviceName = targetServer.name; for (const { description, name, inputSchema, } of targetServer.originalTools) { const id = toToolId(serviceName, name); tools.push({ description, id, inputSchema, name, serviceName, }); } } set({ customTools, tools }); }, setTools: (update) => { set((state) => ({ tools: typeof update === "function" ? update(state.tools) : update, })); }, updateCustomTool: (tool) => { const { appConfig } = socketStore.getState(); if (!appConfig) { throw new Error("App config is not available."); } const toolExtensions = { ...(appConfig.toolExtensions?.services || {}) }; // Ensure the service exists if (!toolExtensions[tool.originalTool.serviceName]) { toolExtensions[tool.originalTool.serviceName] = {}; } // Ensure the original tool exists if ( !toolExtensions[tool.originalTool.serviceName][tool.originalTool.name] ) { toolExtensions[tool.originalTool.serviceName][tool.originalTool.name] = { childTools: [], }; } // Find and update the existing custom tool const childTools = toolExtensions[tool.originalTool.serviceName][tool.originalTool.name] .childTools; // For edit mode, we need to find the tool by the original name, not the new name // because the user might be changing the name const lookupName = tool.originalName || tool.name; const toolIndex = childTools.findIndex((ct: ToolExtension) => { // Use originalName if available (for edit mode), otherwise use current name return ct.name === lookupName; }); if (toolIndex >= 0) { const updatedTool: ToolExtension = { description: tool.description, name: tool.name, overrideParams: Object.fromEntries( Object.entries(tool.overrideParams).filter( ([, value]) => value !== undefined, ), ), }; childTools[toolIndex] = updatedTool; } else { // Tool not found, this shouldn't happen in edit mode } const updates: AppConfig = { ...appConfig, toolExtensions: { services: toolExtensions, }, }; return updates; }, })); // Subscribe to socket store updates to keep tools state // in sync with the app configuration and system state. socketStore.subscribe((state) => { // Only update if there are no pending operations (to prevent overwriting optimistic updates) toolsStore.getState().init(state); }); export const useToolsStore = <T>(selector: (state: ToolsStore) => T) => toolsStore(useShallow(selector)); export { toolsStore }; export const initToolsStore = () => { toolsStore.getState().init(socketStore.getState()); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server