From 8d0ac5d7c97ffd20d611bac794817f6791aace4d Mon Sep 17 00:00:00 2001 From: Sundeep Gottipati Date: Mon, 13 Oct 2025 23:09:08 -0700 Subject: [PATCH] Initial ChatGPT UI --- docs/feature-specs/dynamic_servers.md | 20 +- pkg/gateway/dynamic_mcps.go | 224 +++--- pkg/gateway/reload.go | 8 + pkg/gateway/tool_manager_ui.go | 285 +++++++ pkg/gateway/ui/embedded/tool-manager.js | 255 +++++++ ui/tool-manager/package-lock.json | 552 ++++++++++++++ ui/tool-manager/package.json | 18 + ui/tool-manager/src/index.tsx | 956 ++++++++++++++++++++++++ ui/tool-manager/tsconfig.json | 16 + 9 files changed, 2233 insertions(+), 101 deletions(-) create mode 100644 pkg/gateway/tool_manager_ui.go create mode 100644 pkg/gateway/ui/embedded/tool-manager.js create mode 100644 ui/tool-manager/package-lock.json create mode 100644 ui/tool-manager/package.json create mode 100644 ui/tool-manager/src/index.tsx create mode 100644 ui/tool-manager/tsconfig.json diff --git a/docs/feature-specs/dynamic_servers.md b/docs/feature-specs/dynamic_servers.md index 46bfa824..91a53ad0 100644 --- a/docs/feature-specs/dynamic_servers.md +++ b/docs/feature-specs/dynamic_servers.md @@ -158,6 +158,24 @@ When servers are added dynamically, the system automatically: The `mcp-add` tool ensures server name uniqueness by: - Checking existing server names before adding + +## ChatGPT Tool Manager UI + +When the `dynamic-tools` feature flag is enabled, the gateway now publishes an interactive ChatGPT widget that orchestrates `mcp-find`, `mcp-add`, and `mcp-remove`. The component: + +- Renders catalog search results with descriptions, required secrets, and server metadata. +- Shows the current set of enabled servers and highlights whether each result is active. +- Initiates tool calls directly from the UI to add or remove servers and reflects the outcome in real time. + +The widget bundle lives in `ui/tool-manager/` and is embedded into the gateway at build time. To make changes: + +```bash +cd ui/tool-manager +npm install # first time only +npm run build # rebuilds the bundle and copies it to pkg/gateway/ui/embedded/ +``` + +Running `npm run build` updates both `ui/tool-manager/dist/tool-manager.js` (for development) and the embedded asset at `pkg/gateway/ui/embedded/tool-manager.js`. Re-start the gateway after rebuilding so the new UI is served to ChatGPT. - Only appending new servers if not already present - Maintaining the original order of servers @@ -224,4 +242,4 @@ Potential future improvements include: - Server dependency management - Configuration validation - Server status monitoring -- Rollback capabilities \ No newline at end of file +- Rollback capabilities diff --git a/pkg/gateway/dynamic_mcps.go b/pkg/gateway/dynamic_mcps.go index 93d4ca74..04dd94d6 100644 --- a/pkg/gateway/dynamic_mcps.go +++ b/pkg/gateway/dynamic_mcps.go @@ -29,7 +29,16 @@ import ( func (g *Gateway) createMcpFindTool(configuration Configuration) *ToolRegistration { tool := &mcp.Tool{ Name: "mcp-find", + Title: "Search MCP Catalog", Description: "Find MCP servers in the current catalog by name or description. Returns matching servers with their details.", + Meta: mcp.Meta{ + "openai/outputTemplate": toolManagerResourceURI, + "openai/widgetAccessible": true, + "openai/toolInvocation/invoking": "Searching the MCP catalog…", + "openai/toolInvocation/invoked": "Search complete.", + "openai/toolInvocation/failed": "Catalog search failed.", + "openai/toolInvocation/description": "Searches the MCP catalog and renders results in the Tool Manager UI.", + }, InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -44,6 +53,7 @@ func (g *Gateway) createMcpFindTool(configuration Configuration) *ToolRegistrati }, Required: []string{"query"}, }, + OutputSchema: toolManagerOutputSchema, } handler := func(_ context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -148,53 +158,43 @@ func (g *Gateway) createMcpFindTool(configuration Configuration) *ToolRegistrati } } + totalMatches := len(matches) + // Limit results if len(matches) > params.Limit { matches = matches[:params.Limit] } - // Format results - var results []map[string]any + activeServers := g.activeServerNames() + serverSummaries := make([]toolManagerServer, 0, len(matches)) for _, match := range matches { - serverInfo := map[string]any{ - "name": match.Name, - } - - if match.Server.Description != "" { - serverInfo["description"] = match.Server.Description - } + summary := summarizeServer(match.Name, match.Server) + summary.IsActive = slices.Contains(activeServers, match.Name) + serverSummaries = append(serverSummaries, summary) + } - if len(match.Server.Secrets) > 0 { - var secrets []string - for _, secret := range match.Server.Secrets { - secrets = append(secrets, secret.Name) + message := fmt.Sprintf("Found %d server(s) matching %q.", totalMatches, params.Query) + if len(serverSummaries) > 0 { + var topNames []string + for idx, server := range serverSummaries { + if idx >= 5 { + break } - serverInfo["required_secrets"] = secrets + topNames = append(topNames, server.Name) } - - if len(match.Server.Config) > 0 { - serverInfo["config_schema"] = match.Server.Config + if len(topNames) > 0 { + message = fmt.Sprintf("%s Top matches: %s.", message, strings.Join(topNames, ", ")) } - - serverInfo["long_lived"] = match.Server.LongLived - - results = append(results, serverInfo) } - response := map[string]any{ - "query": params.Query, - "total_matches": len(results), - "servers": results, - } + payload := newToolManagerPayload("mcp-find", "success", message) + payload.Query = params.Query + payload.Limit = params.Limit + payload.TotalMatches = totalMatches + payload.Results = serverSummaries + payload.LastAction = newToolManagerAction("find", "success", message, "") - responseBytes, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{Text: string(responseBytes)}}, - }, nil + return g.buildToolManagerResult(payload, message), nil } return &ToolRegistration{ @@ -214,7 +214,16 @@ type ServerMatch struct { func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration { tool := &mcp.Tool{ Name: "mcp-add", + Title: "Add MCP Server", Description: "Add a new MCP server to the session. The server must exist in the catalog.", + Meta: mcp.Meta{ + "openai/outputTemplate": toolManagerResourceURI, + "openai/widgetAccessible": true, + "openai/toolInvocation/invoking": "Adding the server to your session…", + "openai/toolInvocation/invoked": "Server added.", + "openai/toolInvocation/failed": "Failed to add the server.", + "openai/toolInvocation/description": "Enables an MCP server from the catalog and reloads configuration.", + }, InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -225,10 +234,10 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration }, Required: []string{"name"}, }, + OutputSchema: toolManagerOutputSchema, } handler := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // Parse parameters var params struct { Name string `json:"name"` } @@ -252,36 +261,42 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration serverName := strings.TrimSpace(params.Name) - // Check if server exists in catalog + payload := newToolManagerPayload("mcp-add", "info", "") + payload.Server = &toolManagerServer{Name: serverName} + serverConfig, _, found := g.configuration.Find(serverName) if !found { - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{ - Text: fmt.Sprintf("Error: Server '%s' not found in catalog. Use mcp-find to search for available servers.", serverName), - }}, - }, nil - } - - // Append the new server to the current serverNames if not already present - found = false - for _, existing := range g.configuration.serverNames { - if existing == serverName { - found = true + message := fmt.Sprintf("Error: Server '%s' not found in catalog. Use mcp-find to search for available servers.", serverName) + payload.Status = "error" + payload.Message = message + payload.Error = message + payload.LastAction = newToolManagerAction("add", "error", message, serverName) + return g.buildToolManagerResult(payload, message), nil + } + + serverSummary := summarizeServer(serverName, serverConfig.Spec) + serverSummary.IsActive = true + payload.Server = &serverSummary + payload.Results = []toolManagerServer{serverSummary} + + existing := false + for _, name := range g.configuration.serverNames { + if name == serverName { + existing = true break } } - if !found { + if !existing { g.configuration.serverNames = append(g.configuration.serverNames, serverName) } - // Fetch updated secrets for the new server list if g.configurator != nil { if fbc, ok := g.configurator.(*FileBasedConfiguration); ok { - updatedSecrets, err := fbc.readDockerDesktopSecrets(ctx, g.configuration.servers, g.configuration.serverNames) - if err == nil { + updatedSecrets, secretsErr := fbc.readDockerDesktopSecrets(ctx, g.configuration.servers, g.configuration.serverNames) + if secretsErr == nil { g.configuration.secrets = updatedSecrets } else { - log("Warning: Failed to update secrets:", err) + log("Warning: Failed to update secrets:", secretsErr) } } } @@ -290,20 +305,22 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration return nil, fmt.Errorf("failed to reload configuration: %w", err) } - // Register DCR client and start OAuth provider if this is a remote OAuth server + respond := func(status, message string) (*mcp.CallToolResult, error) { + payload.Status = status + payload.Message = message + payload.LastAction = newToolManagerAction("add", status, message, serverName) + return g.buildToolManagerResult(payload, message), nil + } + if g.McpOAuthDcrEnabled && serverConfig.Spec.IsRemoteOAuthServer() { - // Register DCR client with DD so user can authorize if err := oauth.RegisterProviderForLazySetup(ctx, serverName); err != nil { logf("Warning: Failed to register OAuth provider for %s: %v", serverName, err) } - // Start provider g.startProvider(ctx, serverName) - // Check if current serverSession supports elicitations if req.Session.InitializeParams().Capabilities != nil && req.Session.InitializeParams().Capabilities.Elicitation != nil { - // Elicit a response from the client asking whether to open a browser for authorization - elicitResult, err := req.Session.Elicit(ctx, &mcp.ElicitParams{ + elicitResult, elicitErr := req.Session.Elicit(ctx, &mcp.ElicitParams{ Message: fmt.Sprintf("Would you like to open a browser to authorize the '%s' server?", serverName), RequestedSchema: &jsonschema.Schema{ Type: "object", @@ -316,16 +333,14 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration Required: []string{"authorize"}, }, }) - if err != nil { - logf("Warning: Failed to elicit authorization response for %s: %v", serverName, err) + if elicitErr != nil { + logf("Warning: Failed to elicit authorization response for %s: %v", serverName, elicitErr) } else if elicitResult.Action == "accept" && elicitResult.Content != nil { - // Check if user authorized if authorize, ok := elicitResult.Content["authorize"].(bool); ok && authorize { - // User agreed to authorize, call the OAuth authorize function client := desktop.NewAuthClient() - authResponse, err := client.PostOAuthApp(ctx, serverName, "", false) - if err != nil { - logf("Warning: Failed to start OAuth flow for %s: %v", serverName, err) + authResponse, authErr := client.PostOAuthApp(ctx, serverName, "", false) + if authErr != nil { + logf("Warning: Failed to start OAuth flow for %s: %v", serverName, authErr) } else if authResponse.BrowserURL != "" { logf("Opening browser for authentication: %s", authResponse.BrowserURL) } else { @@ -334,40 +349,22 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration } } - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{ - Text: fmt.Sprintf("Successfully added server '%s'. Authorization completed.", serverName), - }}, - }, nil + return respond("success", fmt.Sprintf("Successfully added server '%s'. Authorization completed.", serverName)) } - // Client doesn't support elicitations, get the login link and include it in the response client := desktop.NewAuthClient() - // Set context flag to enable disableAutoOpen parameter ctxWithFlag := context.WithValue(ctx, contextkeys.OAuthInterceptorEnabledKey, true) - authResponse, err := client.PostOAuthApp(ctxWithFlag, serverName, "", true) - if err != nil { - logf("Warning: Failed to get OAuth URL for %s: %v", serverName, err) + authResponse, authErr := client.PostOAuthApp(ctxWithFlag, serverName, "", true) + if authErr != nil { + logf("Warning: Failed to get OAuth URL for %s: %v", serverName, authErr) } else if authResponse.BrowserURL != "" { - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{ - Text: fmt.Sprintf("Successfully added server '%s'. To authorize this server, please open the following URL in your browser:\n\n%s", serverName, authResponse.BrowserURL), - }}, - }, nil + return respond("success", fmt.Sprintf("Successfully added server '%s'. To authorize this server, please open the following URL in your browser:\n\n%s", serverName, authResponse.BrowserURL)) } - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{ - Text: fmt.Sprintf("Successfully added server '%s'. You will need to authorize this server with: docker mcp oauth authorize %s", serverName, serverName), - }}, - }, nil + return respond("success", fmt.Sprintf("Successfully added server '%s'. You will need to authorize this server with: docker mcp oauth authorize %s", serverName, serverName)) } - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{ - Text: fmt.Sprintf("Successfully added server '%s'. Assume that it is fully configured and ready to use.", serverName), - }}, - }, nil + return respond("success", fmt.Sprintf("Successfully added server '%s'. Assume that it is fully configured and ready to use.", serverName)) } return &ToolRegistration{ @@ -380,7 +377,16 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration func (g *Gateway) createMcpRemoveTool() *ToolRegistration { tool := &mcp.Tool{ Name: "mcp-remove", + Title: "Remove MCP Server", Description: "Remove an MCP server from the registry and reload the configuration. This will disable the server.", + Meta: mcp.Meta{ + "openai/outputTemplate": toolManagerResourceURI, + "openai/widgetAccessible": true, + "openai/toolInvocation/invoking": "Removing the server from your session…", + "openai/toolInvocation/invoked": "Server removed.", + "openai/toolInvocation/failed": "Failed to remove the server.", + "openai/toolInvocation/description": "Disables an MCP server and reloads configuration.", + }, InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -391,6 +397,7 @@ func (g *Gateway) createMcpRemoveTool() *ToolRegistration { }, Required: []string{"name"}, }, + OutputSchema: toolManagerOutputSchema, } handler := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -418,15 +425,15 @@ func (g *Gateway) createMcpRemoveTool() *ToolRegistration { serverName := strings.TrimSpace(params.Name) - // Remove the server from the current serverNames + payload := newToolManagerPayload("mcp-remove", "info", "") + payload.Server = &toolManagerServer{Name: serverName} + updatedServerNames := slices.DeleteFunc(slices.Clone(g.configuration.serverNames), func(name string) bool { return name == serverName }) - - // Update the current configuration state + wasActive := len(updatedServerNames) != len(g.configuration.serverNames) g.configuration.serverNames = updatedServerNames - // Stop OAuth provider if this is an OAuth server if g.McpOAuthDcrEnabled { g.stopProvider(serverName) } @@ -435,11 +442,28 @@ func (g *Gateway) createMcpRemoveTool() *ToolRegistration { return nil, fmt.Errorf("failed to remove server configuration: %w", err) } - return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{ - Text: fmt.Sprintf("Successfully removed server '%s'.", serverName), - }}, - }, nil + if serverDetails, ok := g.configuration.servers[serverName]; ok { + serverSummary := summarizeServer(serverName, serverDetails) + serverSummary.IsActive = false + payload.Server = &serverSummary + payload.Results = []toolManagerServer{serverSummary} + } + + var status string + var message string + if wasActive { + status = "success" + message = fmt.Sprintf("Successfully removed server '%s'.", serverName) + } else { + status = "info" + message = fmt.Sprintf("Server '%s' was not active. No changes were required.", serverName) + } + + payload.Status = status + payload.Message = message + payload.LastAction = newToolManagerAction("remove", status, message, serverName) + + return g.buildToolManagerResult(payload, message), nil } return &ToolRegistration{ diff --git a/pkg/gateway/reload.go b/pkg/gateway/reload.go index 53c35e53..c83436e2 100644 --- a/pkg/gateway/reload.go +++ b/pkg/gateway/reload.go @@ -73,25 +73,33 @@ func (g *Gateway) reloadConfiguration(ctx context.Context, configuration Configu if g.DynamicTools { log("- Adding internal tools (dynamic-tools feature enabled)") + dynamicCaps := g.ensureDynamicCapabilities() + g.registerToolManagerResource() + // Add mcp-find tool mcpFindTool := g.createMcpFindTool(configuration) g.mcpServer.AddTool(mcpFindTool.Tool, mcpFindTool.Handler) + dynamicCaps.ToolNames = append(dynamicCaps.ToolNames, mcpFindTool.Tool.Name) // Add mcp-add tool mcpAddTool := g.createMcpAddTool(clientConfig) g.mcpServer.AddTool(mcpAddTool.Tool, mcpAddTool.Handler) + dynamicCaps.ToolNames = append(dynamicCaps.ToolNames, mcpAddTool.Tool.Name) // Add mcp-remove tool mcpRemoveTool := g.createMcpRemoveTool() g.mcpServer.AddTool(mcpRemoveTool.Tool, mcpRemoveTool.Handler) + dynamicCaps.ToolNames = append(dynamicCaps.ToolNames, mcpRemoveTool.Tool.Name) // Add mcp-registry-import tool mcpRegistryImportTool := g.createMcpRegistryImportTool(configuration, clientConfig) g.mcpServer.AddTool(mcpRegistryImportTool.Tool, mcpRegistryImportTool.Handler) + dynamicCaps.ToolNames = append(dynamicCaps.ToolNames, mcpRegistryImportTool.Tool.Name) // Add mcp-config-set tool mcpConfigSetTool := g.createMcpConfigSetTool(clientConfig) g.mcpServer.AddTool(mcpConfigSetTool.Tool, mcpConfigSetTool.Handler) + dynamicCaps.ToolNames = append(dynamicCaps.ToolNames, mcpConfigSetTool.Tool.Name) log(" > mcp-find: tool for finding MCP servers in the catalog") log(" > mcp-add: tool for adding MCP servers to the registry") diff --git a/pkg/gateway/tool_manager_ui.go b/pkg/gateway/tool_manager_ui.go new file mode 100644 index 00000000..67058b9c --- /dev/null +++ b/pkg/gateway/tool_manager_ui.go @@ -0,0 +1,285 @@ +package gateway + +import ( + "context" + "fmt" + "slices" + "strings" + "time" + + _ "embed" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "github.com/docker/mcp-gateway/pkg/catalog" +) + +const ( + dynamicToolsCapabilityKey = "__dynamic_tools__" + + toolManagerView = "mcp-tool-manager" + toolManagerResourceURI = "ui://widget/mcp-tool-manager.html" + toolManagerResourceName = "mcp-tool-manager-ui" + toolManagerResourceMIMEType = "text/html+skybridge" +) + +//go:embed ui/embedded/tool-manager.js +var toolManagerBundle string + +var toolManagerOutputSchema = map[string]any{ + "type": "object", + "properties": map[string]any{ + "view": map[string]any{"type": "string"}, + "status": map[string]any{"type": "string"}, + "message": map[string]any{"type": "string"}, + "query": map[string]any{"type": "string"}, + "limit": map[string]any{"type": "integer"}, + "totalMatches": map[string]any{"type": "integer"}, + "activeServers": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, + "results": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + "description": map[string]any{"type": "string"}, + "type": map[string]any{"type": "string"}, + "remoteUrl": map[string]any{"type": "string"}, + "image": map[string]any{"type": "string"}, + "longLived": map[string]any{"type": "boolean"}, + "requiredSecrets": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, + "isActive": map[string]any{"type": "boolean"}, + }, + }, + }, + "server": map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + }, + }, + "lastAction": map[string]any{ + "type": "object", + "properties": map[string]any{ + "type": map[string]any{"type": "string"}, + "status": map[string]any{"type": "string"}, + "message": map[string]any{"type": "string"}, + "server": map[string]any{"type": "string"}, + }, + }, + "timestamp": map[string]any{"type": "string"}, + "error": map[string]any{"type": "string"}, + }, +} + +type toolManagerServer struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Type string `json:"type,omitempty"` + RemoteURL string `json:"remoteUrl,omitempty"` + Image string `json:"image,omitempty"` + LongLived bool `json:"longLived,omitempty"` + RequiredSecrets []string `json:"requiredSecrets,omitempty"` + ConfigSchema []any `json:"configSchema,omitempty"` + Tools []toolManagerServerTool `json:"tools,omitempty"` + OAuthProviders []string `json:"oauthProviders,omitempty"` + IsActive bool `json:"isActive,omitempty"` +} + +type toolManagerServerTool struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` +} + +type toolManagerAction struct { + Type string `json:"type"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + Server string `json:"server,omitempty"` +} + +type toolManagerPayload struct { + View string `json:"view"` + SourceTool string `json:"sourceTool"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + Query string `json:"query,omitempty"` + Limit int `json:"limit,omitempty"` + TotalMatches int `json:"totalMatches,omitempty"` + ActiveServers []string `json:"activeServers"` + Results []toolManagerServer `json:"results,omitempty"` + Server *toolManagerServer `json:"server,omitempty"` + LastAction *toolManagerAction `json:"lastAction,omitempty"` + Timestamp string `json:"timestamp"` + Error string `json:"error,omitempty"` +} + +func ensureToolManagerBundle() string { + escaped := strings.ReplaceAll(toolManagerBundle, "", "<\\/script>") + return fmt.Sprintf(`
`, escaped) +} + +func (g *Gateway) ensureDynamicCapabilities() *ServerCapabilities { + if g.serverCapabilities[dynamicToolsCapabilityKey] == nil { + g.serverCapabilities[dynamicToolsCapabilityKey] = &ServerCapabilities{} + } + return g.serverCapabilities[dynamicToolsCapabilityKey] +} + +func (g *Gateway) registerToolManagerResource() { + resource := &mcp.Resource{ + Name: toolManagerResourceName, + URI: toolManagerResourceURI, + Title: "MCP Tool Manager", + Description: "Interactive UI for searching, adding, and removing MCP servers in the gateway.", + MIMEType: toolManagerResourceMIMEType, + Meta: mcp.Meta{ + "openai/widgetDescription": "Interactive UI for searching the MCP catalog, enabling servers, and removing them from the session.", + }, + } + + g.mcpServer.AddResource(resource, func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + { + URI: toolManagerResourceURI, + MIMEType: toolManagerResourceMIMEType, + Text: ensureToolManagerBundle(), + }, + }, + }, nil + }) + + caps := g.ensureDynamicCapabilities() + caps.ResourceURIs = append(caps.ResourceURIs, toolManagerResourceURI) +} + +func (g *Gateway) activeServerNames() []string { + seen := map[string]struct{}{} + var names []string + for _, name := range g.configuration.serverNames { + name = strings.TrimSpace(name) + if name == "" { + continue + } + if _, exists := seen[name]; exists { + continue + } + seen[name] = struct{}{} + names = append(names, name) + } + slices.Sort(names) + return names +} + +func summarizeServer(name string, server catalog.Server) toolManagerServer { + summary := toolManagerServer{ + Name: name, + Description: server.Description, + Type: server.Type, + Image: server.Image, + LongLived: server.LongLived, + } + + if server.Remote.URL != "" { + summary.RemoteURL = server.Remote.URL + } else if server.SSEEndpoint != "" { + summary.RemoteURL = server.SSEEndpoint + } + + if len(server.Secrets) > 0 { + secrets := make([]string, 0, len(server.Secrets)) + for _, secret := range server.Secrets { + if secret.Name != "" { + secrets = append(secrets, secret.Name) + } + } + summary.RequiredSecrets = secrets + } + + if len(server.Tools) > 0 { + tools := make([]toolManagerServerTool, 0, len(server.Tools)) + for _, tool := range server.Tools { + tools = append(tools, toolManagerServerTool{ + Name: tool.Name, + Description: tool.Description, + }) + } + summary.Tools = tools + } + + if len(server.Config) > 0 { + summary.ConfigSchema = server.Config + } + + if server.OAuth != nil && len(server.OAuth.Providers) > 0 { + providers := make([]string, 0, len(server.OAuth.Providers)) + for _, provider := range server.OAuth.Providers { + if provider.Provider != "" { + providers = append(providers, provider.Provider) + } + } + summary.OAuthProviders = providers + } + + return summary +} + +func newToolManagerPayload(sourceTool, status, message string) toolManagerPayload { + if status == "" { + status = "info" + } + payload := toolManagerPayload{ + View: toolManagerView, + SourceTool: sourceTool, + Status: status, + Message: message, + ActiveServers: []string{}, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + return payload +} + +func newToolManagerAction(actionType, status, message, server string) *toolManagerAction { + if actionType == "" { + actionType = "find" + } + if status != "error" { + status = "success" + } + return &toolManagerAction{ + Type: actionType, + Status: status, + Message: message, + Server: server, + } +} + +func (g *Gateway) buildToolManagerResult(payload toolManagerPayload, contentMessage string) *mcp.CallToolResult { + payload.ActiveServers = g.activeServerNames() + if payload.LastAction == nil && payload.SourceTool != "" { + actionType := "find" + switch payload.SourceTool { + case "mcp-add": + actionType = "add" + case "mcp-remove": + actionType = "remove" + } + serverName := "" + if payload.Server != nil { + serverName = payload.Server.Name + } + payload.LastAction = newToolManagerAction(actionType, payload.Status, payload.Message, serverName) + } + + result := &mcp.CallToolResult{ + Meta: mcp.Meta{ + "openai/outputTemplate": toolManagerResourceURI, + }, + Content: []mcp.Content{ + &mcp.TextContent{Text: contentMessage}, + }, + StructuredContent: payload, + } + return result +} diff --git a/pkg/gateway/ui/embedded/tool-manager.js b/pkg/gateway/ui/embedded/tool-manager.js new file mode 100644 index 00000000..2ba8f263 --- /dev/null +++ b/pkg/gateway/ui/embedded/tool-manager.js @@ -0,0 +1,255 @@ +var Xc=Object.create;var tu=Object.defineProperty;var Zc=Object.getOwnPropertyDescriptor;var qc=Object.getOwnPropertyNames;var Jc=Object.getPrototypeOf,bc=Object.prototype.hasOwnProperty;var Ve=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var ef=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of qc(t))!bc.call(e,l)&&l!==n&&tu(e,l,{get:()=>t[l],enumerable:!(r=Zc(t,l))||r.enumerable});return e};var rr=(e,t,n)=>(n=e!=null?Xc(Jc(e)):{},ef(t||!e||!e.__esModule?tu(n,"default",{value:e,enumerable:!0}):n,e));var pu=Ve(N=>{"use strict";var fn=Symbol.for("react.element"),tf=Symbol.for("react.portal"),nf=Symbol.for("react.fragment"),rf=Symbol.for("react.strict_mode"),lf=Symbol.for("react.profiler"),of=Symbol.for("react.provider"),uf=Symbol.for("react.context"),sf=Symbol.for("react.forward_ref"),af=Symbol.for("react.suspense"),cf=Symbol.for("react.memo"),ff=Symbol.for("react.lazy"),nu=Symbol.iterator;function df(e){return e===null||typeof e!="object"?null:(e=nu&&e[nu]||e["@@iterator"],typeof e=="function"?e:null)}var ou={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},iu=Object.assign,uu={};function Dt(e,t,n){this.props=e,this.context=t,this.refs=uu,this.updater=n||ou}Dt.prototype.isReactComponent={};Dt.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Dt.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function su(){}su.prototype=Dt.prototype;function Ml(e,t,n){this.props=e,this.context=t,this.refs=uu,this.updater=n||ou}var Ol=Ml.prototype=new su;Ol.constructor=Ml;iu(Ol,Dt.prototype);Ol.isPureReactComponent=!0;var ru=Array.isArray,au=Object.prototype.hasOwnProperty,Dl={current:null},cu={key:!0,ref:!0,__self:!0,__source:!0};function fu(e,t,n){var r,l={},o=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(o=""+t.key),t)au.call(t,r)&&!cu.hasOwnProperty(r)&&(l[r]=t[r]);var u=arguments.length-2;if(u===1)l.children=n;else if(1{"use strict";mu.exports=pu()});var _u=Ve(R=>{"use strict";function Ul(e,t){var n=e.length;e.push(t);e:for(;0>>1,l=e[r];if(0>>1;rsr(u,n))ssr(c,u)?(e[r]=c,e[s]=n,r=s):(e[r]=u,e[i]=n,r=i);else if(ssr(c,n))e[r]=c,e[s]=n,r=s;else break e}}return t}function sr(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}typeof performance=="object"&&typeof performance.now=="function"?(gu=performance,R.unstable_now=function(){return gu.now()}):(Fl=Date,vu=Fl.now(),R.unstable_now=function(){return Fl.now()-vu});var gu,Fl,vu,De=[],qe=[],hf=1,ye=null,J=3,fr=!1,wt=!1,pn=!1,wu=typeof setTimeout=="function"?setTimeout:null,Su=typeof clearTimeout=="function"?clearTimeout:null,hu=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Vl(e){for(var t=xe(qe);t!==null;){if(t.callback===null)cr(qe);else if(t.startTime<=e)cr(qe),t.sortIndex=t.expirationTime,Ul(De,t);else break;t=xe(qe)}}function $l(e){if(pn=!1,Vl(e),!wt)if(xe(De)!==null)wt=!0,Bl(Wl);else{var t=xe(qe);t!==null&&Hl($l,t.startTime-e)}}function Wl(e,t){wt=!1,pn&&(pn=!1,Su(mn),mn=-1),fr=!0;var n=J;try{for(Vl(t),ye=xe(De);ye!==null&&(!(ye.expirationTime>t)||e&&!Cu());){var r=ye.callback;if(typeof r=="function"){ye.callback=null,J=ye.priorityLevel;var l=r(ye.expirationTime<=t);t=R.unstable_now(),typeof l=="function"?ye.callback=l:ye===xe(De)&&cr(De),Vl(t)}else cr(De);ye=xe(De)}if(ye!==null)var o=!0;else{var i=xe(qe);i!==null&&Hl($l,i.startTime-t),o=!1}return o}finally{ye=null,J=n,fr=!1}}var dr=!1,ar=null,mn=-1,ku=5,Eu=-1;function Cu(){return!(R.unstable_now()-Eue||125r?(e.sortIndex=n,Ul(qe,e),xe(De)===null&&e===xe(qe)&&(pn?(Su(mn),mn=-1):pn=!0,Hl($l,n-r))):(e.sortIndex=l,Ul(De,e),wt||fr||(wt=!0,Bl(Wl))),e};R.unstable_shouldYield=Cu;R.unstable_wrapCallback=function(e){var t=J;return function(){var n=J;J=t;try{return e.apply(this,arguments)}finally{J=n}}}});var Tu=Ve((Fp,xu)=>{"use strict";xu.exports=_u()});var Lc=Ve(he=>{"use strict";var yf=ur(),ge=Tu();function h(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),mo=Object.prototype.hasOwnProperty,wf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Nu={},Pu={};function Sf(e){return mo.call(Pu,e)?!0:mo.call(Nu,e)?!1:wf.test(e)?Pu[e]=!0:(Nu[e]=!0,!1)}function kf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Ef(e,t,n,r){if(t===null||typeof t>"u"||kf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function ie(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var q={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){q[e]=new ie(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];q[t]=new ie(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){q[e]=new ie(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){q[e]=new ie(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){q[e]=new ie(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){q[e]=new ie(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){q[e]=new ie(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){q[e]=new ie(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){q[e]=new ie(e,5,!1,e.toLowerCase(),null,!1,!1)});var ii=/[\-:]([a-z])/g;function ui(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ii,ui);q[t]=new ie(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ii,ui);q[t]=new ie(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ii,ui);q[t]=new ie(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){q[e]=new ie(e,1,!1,e.toLowerCase(),null,!1,!1)});q.xlinkHref=new ie("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){q[e]=new ie(e,1,!1,e.toLowerCase(),null,!0,!0)});function si(e,t,n,r){var l=q.hasOwnProperty(t)?q[t]:null;(l!==null?l.type!==0:r||!(2u||l[i]!==o[u]){var s=` +`+l[i].replace(" at new "," at ");return e.displayName&&s.includes("")&&(s=s.replace("",e.displayName)),s}while(1<=i&&0<=u);break}}}finally{Kl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Cn(e):""}function Cf(e){switch(e.tag){case 5:return Cn(e.type);case 16:return Cn("Lazy");case 13:return Cn("Suspense");case 19:return Cn("SuspenseList");case 0:case 2:case 15:return e=Yl(e.type,!1),e;case 11:return e=Yl(e.type.render,!1),e;case 1:return e=Yl(e.type,!0),e;default:return""}}function yo(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case jt:return"Fragment";case At:return"Portal";case go:return"Profiler";case ai:return"StrictMode";case vo:return"Suspense";case ho:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Is:return(e.displayName||"Context")+".Consumer";case Ds:return(e._context.displayName||"Context")+".Provider";case ci:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case fi:return t=e.displayName||null,t!==null?t:yo(e.type)||"Memo";case be:t=e._payload,e=e._init;try{return yo(e(t))}catch{}}return null}function _f(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return yo(t);case 8:return t===ai?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function pt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function As(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function xf(e){var t=As(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function mr(e){e._valueTracker||(e._valueTracker=xf(e))}function js(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=As(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Wr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function wo(e,t){var n=t.checked;return U({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Lu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=pt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Us(e,t){t=t.checked,t!=null&&si(e,"checked",t,!1)}function So(e,t){Us(e,t);var n=pt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?ko(e,t.type,n):t.hasOwnProperty("defaultValue")&&ko(e,t.type,pt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Ru(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function ko(e,t,n){(t!=="number"||Wr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var _n=Array.isArray;function Xt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=gr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function An(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Nn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Tf=["Webkit","ms","Moz","O"];Object.keys(Nn).forEach(function(e){Tf.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Nn[t]=Nn[e]})});function Bs(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Nn.hasOwnProperty(e)&&Nn[e]?(""+t).trim():t+"px"}function Hs(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Bs(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Nf=U({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function _o(e,t){if(t){if(Nf[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(h(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(h(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(h(61))}if(t.style!=null&&typeof t.style!="object")throw Error(h(62))}}function xo(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var To=null;function di(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var No=null,Zt=null,qt=null;function Du(e){if(e=tr(e)){if(typeof No!="function")throw Error(h(280));var t=e.stateNode;t&&(t=vl(t),No(e.stateNode,e.type,t))}}function Qs(e){Zt?qt?qt.push(e):qt=[e]:Zt=e}function Ks(){if(Zt){var e=Zt,t=qt;if(qt=Zt=null,Du(e),t)for(e=0;e>>=0,e===0?32:31-(jf(e)/Uf|0)|0}var vr=64,hr=4194304;function xn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Kr(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var u=i&~l;u!==0?r=xn(u):(o&=i,o!==0&&(r=xn(o)))}else i=n&~l,i!==0?r=xn(i):o!==0&&(r=xn(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&l)&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function bn(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Le(t),e[t]=n}function Bf(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=zn),Bu=" ",Hu=!1;function da(e,t){switch(e){case"keyup":return hd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function pa(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ut=!1;function wd(e,t){switch(e){case"compositionend":return pa(t);case"keypress":return t.which!==32?null:(Hu=!0,Bu);case"textInput":return e=t.data,e===Bu&&Hu?null:e;default:return null}}function Sd(e,t){if(Ut)return e==="compositionend"||!Si&&da(e,t)?(e=ca(),Or=hi=rt=null,Ut=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Yu(n)}}function ha(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ha(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ya(){for(var e=window,t=Wr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Wr(e.document)}return t}function ki(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function zd(e){var t=ya(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ha(n.ownerDocument.documentElement,n)){if(r!==null&&ki(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=Gu(n,o);var i=Gu(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Vt=null,Oo=null,Rn=null,Do=!1;function Xu(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Do||Vt==null||Vt!==Wr(r)||(r=Vt,"selectionStart"in r&&ki(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Rn&&Bn(Rn,r)||(Rn=r,r=Xr(Oo,"onSelect"),0Bt||(e.current=Vo[Bt],Vo[Bt]=null,Bt--)}function M(e,t){Bt++,Vo[Bt]=e.current,e.current=t}var mt={},ne=vt(mt),ae=vt(!1),Nt=mt;function nn(e,t){var n=e.type.contextTypes;if(!n)return mt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ce(e){return e=e.childContextTypes,e!=null}function qr(){D(ae),D(ne)}function ls(e,t,n){if(ne.current!==mt)throw Error(h(168));M(ne,t),M(ae,n)}function Na(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(h(108,_f(e)||"Unknown",l));return U({},n,r)}function Jr(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||mt,Nt=ne.current,M(ne,e),M(ae,ae.current),!0}function os(e,t,n){var r=e.stateNode;if(!r)throw Error(h(169));n?(e=Na(e,t,Nt),r.__reactInternalMemoizedMergedChildContext=e,D(ae),D(ne),M(ne,e)):D(ae),M(ae,n)}var We=null,hl=!1,lo=!1;function Pa(e){We===null?We=[e]:We.push(e)}function Ud(e){hl=!0,Pa(e)}function ht(){if(!lo&&We!==null){lo=!0;var e=0,t=L;try{var n=We;for(L=1;e>=i,l-=i,Be=1<<32-Le(t)+l|n<T?(B=_,_=null):B=_.sibling;var P=g(f,_,d[T],p);if(P===null){_===null&&(_=B);break}e&&_&&P.alternate===null&&t(f,_),a=o(P,a,T),E===null?y=P:E.sibling=P,E=P,_=B}if(T===d.length)return n(f,_),I&&St(f,T),y;if(_===null){for(;TT?(B=_,_=null):B=_.sibling;var Oe=g(f,_,P.value,p);if(Oe===null){_===null&&(_=B);break}e&&_&&Oe.alternate===null&&t(f,_),a=o(Oe,a,T),E===null?y=Oe:E.sibling=Oe,E=Oe,_=B}if(P.done)return n(f,_),I&&St(f,T),y;if(_===null){for(;!P.done;T++,P=d.next())P=v(f,P.value,p),P!==null&&(a=o(P,a,T),E===null?y=P:E.sibling=P,E=P);return I&&St(f,T),y}for(_=r(f,_);!P.done;T++,P=d.next())P=w(_,f,T,P.value,p),P!==null&&(e&&P.alternate!==null&&_.delete(P.key===null?T:P.key),a=o(P,a,T),E===null?y=P:E.sibling=P,E=P);return e&&_.forEach(function(Ll){return t(f,Ll)}),I&&St(f,T),y}function F(f,a,d,p){if(typeof d=="object"&&d!==null&&d.type===jt&&d.key===null&&(d=d.props.children),typeof d=="object"&&d!==null){switch(d.$$typeof){case pr:e:{for(var y=d.key,E=a;E!==null;){if(E.key===y){if(y=d.type,y===jt){if(E.tag===7){n(f,E.sibling),a=l(E,d.props.children),a.return=f,f=a;break e}}else if(E.elementType===y||typeof y=="object"&&y!==null&&y.$$typeof===be&&ss(y)===E.type){n(f,E.sibling),a=l(E,d.props),a.ref=wn(f,E,d),a.return=f,f=a;break e}n(f,E);break}else t(f,E);E=E.sibling}d.type===jt?(a=Tt(d.props.children,f.mode,p,d.key),a.return=f,f=a):(p=$r(d.type,d.key,d.props,null,f.mode,p),p.ref=wn(f,a,d),p.return=f,f=p)}return i(f);case At:e:{for(E=d.key;a!==null;){if(a.key===E)if(a.tag===4&&a.stateNode.containerInfo===d.containerInfo&&a.stateNode.implementation===d.implementation){n(f,a.sibling),a=l(a,d.children||[]),a.return=f,f=a;break e}else{n(f,a);break}else t(f,a);a=a.sibling}a=po(d,f.mode,p),a.return=f,f=a}return i(f);case be:return E=d._init,F(f,a,E(d._payload),p)}if(_n(d))return S(f,a,d,p);if(gn(d))return C(f,a,d,p);Pr(f,d)}return typeof d=="string"&&d!==""||typeof d=="number"?(d=""+d,a!==null&&a.tag===6?(n(f,a.sibling),a=l(a,d),a.return=f,f=a):(n(f,a),a=fo(d,f.mode,p),a.return=f,f=a),i(f)):n(f,a)}return F}var ln=Ma(!0),Oa=Ma(!1),tl=vt(null),nl=null,Kt=null,xi=null;function Ti(){xi=Kt=nl=null}function Ni(e){var t=tl.current;D(tl),e._currentValue=t}function Bo(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function bt(e,t){nl=e,xi=Kt=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(se=!0),e.firstContext=null)}function Ce(e){var t=e._currentValue;if(xi!==e)if(e={context:e,memoizedValue:t,next:null},Kt===null){if(nl===null)throw Error(h(308));Kt=e,nl.dependencies={lanes:0,firstContext:e}}else Kt=Kt.next=e;return t}var Ct=null;function Pi(e){Ct===null?Ct=[e]:Ct.push(e)}function Da(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Pi(t)):(n.next=l.next,l.next=n),t.interleaved=n,Ge(e,r)}function Ge(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var et=!1;function zi(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ia(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Qe(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function at(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,z&2){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Ge(e,n)}return l=r.interleaved,l===null?(t.next=t,Pi(r)):(t.next=l.next,l.next=t),r.interleaved=t,Ge(e,n)}function Ir(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,mi(e,n)}}function as(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function rl(e,t,n,r){var l=e.updateQueue;et=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,u=l.shared.pending;if(u!==null){l.shared.pending=null;var s=u,c=s.next;s.next=null,i===null?o=c:i.next=c,i=s;var m=e.alternate;m!==null&&(m=m.updateQueue,u=m.lastBaseUpdate,u!==i&&(u===null?m.firstBaseUpdate=c:u.next=c,m.lastBaseUpdate=s))}if(o!==null){var v=l.baseState;i=0,m=c=s=null,u=o;do{var g=u.lane,w=u.eventTime;if((r&g)===g){m!==null&&(m=m.next={eventTime:w,lane:0,tag:u.tag,payload:u.payload,callback:u.callback,next:null});e:{var S=e,C=u;switch(g=t,w=n,C.tag){case 1:if(S=C.payload,typeof S=="function"){v=S.call(w,v,g);break e}v=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=C.payload,g=typeof S=="function"?S.call(w,v,g):S,g==null)break e;v=U({},v,g);break e;case 2:et=!0}}u.callback!==null&&u.lane!==0&&(e.flags|=64,g=l.effects,g===null?l.effects=[u]:g.push(u))}else w={eventTime:w,lane:g,tag:u.tag,payload:u.payload,callback:u.callback,next:null},m===null?(c=m=w,s=v):m=m.next=w,i|=g;if(u=u.next,u===null){if(u=l.shared.pending,u===null)break;g=u,u=g.next,g.next=null,l.lastBaseUpdate=g,l.shared.pending=null}}while(!0);if(m===null&&(s=v),l.baseState=s,l.firstBaseUpdate=c,l.lastBaseUpdate=m,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Lt|=i,e.lanes=i,e.memoizedState=v}}function cs(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=io.transition;io.transition={};try{e(!1),t()}finally{L=n,io.transition=r}}function Ja(){return _e().memoizedState}function Bd(e,t,n){var r=ft(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ba(e))ec(t,n);else if(n=Da(e,t,n,r),n!==null){var l=oe();Re(n,e,r,l),tc(n,t,r)}}function Hd(e,t,n){var r=ft(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ba(e))ec(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,u=o(i,n);if(l.hasEagerState=!0,l.eagerState=u,Me(u,i)){var s=t.interleaved;s===null?(l.next=l,Pi(t)):(l.next=s.next,s.next=l),t.interleaved=l;return}}catch{}finally{}n=Da(e,t,l,r),n!==null&&(l=oe(),Re(n,e,r,l),tc(n,t,r))}}function ba(e){var t=e.alternate;return e===j||t!==null&&t===j}function ec(e,t){Mn=ol=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function tc(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,mi(e,n)}}var il={readContext:Ce,useCallback:b,useContext:b,useEffect:b,useImperativeHandle:b,useInsertionEffect:b,useLayoutEffect:b,useMemo:b,useReducer:b,useRef:b,useState:b,useDebugValue:b,useDeferredValue:b,useTransition:b,useMutableSource:b,useSyncExternalStore:b,useId:b,unstable_isNewReconciler:!1},Qd={readContext:Ce,useCallback:function(e,t){return Fe().memoizedState=[e,t===void 0?null:t],e},useContext:Ce,useEffect:ds,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Ar(4194308,4,Ya.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ar(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ar(4,2,e,t)},useMemo:function(e,t){var n=Fe();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Fe();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Bd.bind(null,j,e),[r.memoizedState,e]},useRef:function(e){var t=Fe();return e={current:e},t.memoizedState=e},useState:fs,useDebugValue:Ai,useDeferredValue:function(e){return Fe().memoizedState=e},useTransition:function(){var e=fs(!1),t=e[0];return e=Wd.bind(null,e[1]),Fe().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=j,l=Fe();if(I){if(n===void 0)throw Error(h(407));n=n()}else{if(n=t(),G===null)throw Error(h(349));zt&30||Ua(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,ds($a.bind(null,r,o,e),[e]),r.flags|=2048,qn(9,Va.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Fe(),t=G.identifierPrefix;if(I){var n=He,r=Be;n=(r&~(1<<32-Le(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Xn++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[Ae]=t,e[Kn]=r,fc(e,t,!1,!1),t.stateNode=e;e:{switch(i=xo(n,r),n){case"dialog":O("cancel",e),O("close",e),l=r;break;case"iframe":case"object":case"embed":O("load",e),l=r;break;case"video":case"audio":for(l=0;lsn&&(t.flags|=128,r=!0,Sn(o,!1),t.lanes=4194304)}else{if(!r)if(e=ll(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Sn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!I)return ee(t),null}else 2*$()-o.renderingStartTime>sn&&n!==1073741824&&(t.flags|=128,r=!0,Sn(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=$(),t.sibling=null,n=A.current,M(A,r?n&1|2:n&1),t):(ee(t),null);case 22:case 23:return Bi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?de&1073741824&&(ee(t),t.subtreeFlags&6&&(t.flags|=8192)):ee(t),null;case 24:return null;case 25:return null}throw Error(h(156,t.tag))}function bd(e,t){switch(Ci(t),t.tag){case 1:return ce(t.type)&&qr(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return on(),D(ae),D(ne),Mi(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ri(t),null;case 13:if(D(A),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(h(340));rn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return D(A),null;case 4:return on(),null;case 10:return Ni(t.type._context),null;case 22:case 23:return Bi(),null;case 24:return null;default:return null}}var Lr=!1,te=!1,ep=typeof WeakSet=="function"?WeakSet:Set,k=null;function Yt(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){V(e,t,r)}else n.current=null}function Jo(e,t,n){try{n()}catch(r){V(e,t,r)}}var Cs=!1;function tp(e,t){if(Io=Yr,e=ya(),ki(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,u=-1,s=-1,c=0,m=0,v=e,g=null;t:for(;;){for(var w;v!==n||l!==0&&v.nodeType!==3||(u=i+l),v!==o||r!==0&&v.nodeType!==3||(s=i+r),v.nodeType===3&&(i+=v.nodeValue.length),(w=v.firstChild)!==null;)g=v,v=w;for(;;){if(v===e)break t;if(g===n&&++c===l&&(u=i),g===o&&++m===r&&(s=i),(w=v.nextSibling)!==null)break;v=g,g=v.parentNode}v=w}n=u===-1||s===-1?null:{start:u,end:s}}else n=null}n=n||{start:0,end:0}}else n=null;for(Fo={focusedElem:e,selectionRange:n},Yr=!1,k=t;k!==null;)if(t=k,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,k=e;else for(;k!==null;){t=k;try{var S=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var C=S.memoizedProps,F=S.memoizedState,f=t.stateNode,a=f.getSnapshotBeforeUpdate(t.elementType===t.type?C:Ne(t.type,C),F);f.__reactInternalSnapshotBeforeUpdate=a}break;case 3:var d=t.stateNode.containerInfo;d.nodeType===1?d.textContent="":d.nodeType===9&&d.documentElement&&d.removeChild(d.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(h(163))}}catch(p){V(t,t.return,p)}if(e=t.sibling,e!==null){e.return=t.return,k=e;break}k=t.return}return S=Cs,Cs=!1,S}function On(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Jo(t,n,o)}l=l.next}while(l!==r)}}function Sl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function bo(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function mc(e){var t=e.alternate;t!==null&&(e.alternate=null,mc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ae],delete t[Kn],delete t[Uo],delete t[Ad],delete t[jd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function gc(e){return e.tag===5||e.tag===3||e.tag===4}function _s(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||gc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function ei(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Zr));else if(r!==4&&(e=e.child,e!==null))for(ei(e,t,n),e=e.sibling;e!==null;)ei(e,t,n),e=e.sibling}function ti(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(ti(e,t,n),e=e.sibling;e!==null;)ti(e,t,n),e=e.sibling}var X=null,Pe=!1;function Je(e,t,n){for(n=n.child;n!==null;)vc(e,t,n),n=n.sibling}function vc(e,t,n){if(je&&typeof je.onCommitFiberUnmount=="function")try{je.onCommitFiberUnmount(dl,n)}catch{}switch(n.tag){case 5:te||Yt(n,t);case 6:var r=X,l=Pe;X=null,Je(e,t,n),X=r,Pe=l,X!==null&&(Pe?(e=X,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):X.removeChild(n.stateNode));break;case 18:X!==null&&(Pe?(e=X,n=n.stateNode,e.nodeType===8?ro(e.parentNode,n):e.nodeType===1&&ro(e,n),$n(e)):ro(X,n.stateNode));break;case 4:r=X,l=Pe,X=n.stateNode.containerInfo,Pe=!0,Je(e,t,n),X=r,Pe=l;break;case 0:case 11:case 14:case 15:if(!te&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&(o&2||o&4)&&Jo(n,t,i),l=l.next}while(l!==r)}Je(e,t,n);break;case 1:if(!te&&(Yt(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(u){V(n,t,u)}Je(e,t,n);break;case 21:Je(e,t,n);break;case 22:n.mode&1?(te=(r=te)||n.memoizedState!==null,Je(e,t,n),te=r):Je(e,t,n);break;default:Je(e,t,n)}}function xs(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new ep),t.forEach(function(r){var l=cp.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Te(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=$()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*rp(r/1960))-r,10e?16:e,lt===null)var r=!1;else{if(e=lt,lt=null,al=0,z&6)throw Error(h(331));var l=z;for(z|=4,k=e.current;k!==null;){var o=k,i=o.child;if(k.flags&16){var u=o.deletions;if(u!==null){for(var s=0;s$()-$i?xt(e,0):Vi|=n),fe(e,t)}function _c(e,t){t===0&&(e.mode&1?(t=hr,hr<<=1,!(hr&130023424)&&(hr=4194304)):t=1);var n=oe();e=Ge(e,t),e!==null&&(bn(e,t,n),fe(e,n))}function ap(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),_c(e,n)}function cp(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(h(314))}r!==null&&r.delete(t),_c(e,n)}var xc;xc=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ae.current)se=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return se=!1,qd(e,t,n);se=!!(e.flags&131072)}else se=!1,I&&t.flags&1048576&&za(t,el,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;jr(e,t),e=t.pendingProps;var l=nn(t,ne.current);bt(t,n),l=Di(null,t,r,e,l,n);var o=Ii();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ce(r)?(o=!0,Jr(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,zi(t),l.updater=wl,t.stateNode=l,l._reactInternals=t,Qo(t,r,e,n),t=Go(null,t,r,!0,o,n)):(t.tag=0,I&&o&&Ei(t),le(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(jr(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=dp(r),e=Ne(r,e),l){case 0:t=Yo(null,t,r,e,n);break e;case 1:t=Ss(null,t,r,e,n);break e;case 11:t=ys(null,t,r,e,n);break e;case 14:t=ws(null,t,r,Ne(r.type,e),n);break e}throw Error(h(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ne(r,l),Yo(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ne(r,l),Ss(e,t,r,l,n);case 3:e:{if(sc(t),e===null)throw Error(h(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Ia(e,t),rl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=un(Error(h(423)),t),t=ks(e,t,r,n,l);break e}else if(r!==l){l=un(Error(h(424)),t),t=ks(e,t,r,n,l);break e}else for(pe=st(t.stateNode.containerInfo.firstChild),me=t,I=!0,ze=null,n=Oa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(rn(),r===l){t=Xe(e,t,n);break e}le(e,t,r,n)}t=t.child}return t;case 5:return Fa(t),e===null&&Wo(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,Ao(r,l)?i=null:o!==null&&Ao(r,o)&&(t.flags|=32),uc(e,t),le(e,t,i,n),t.child;case 6:return e===null&&Wo(t),null;case 13:return ac(e,t,n);case 4:return Li(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=ln(t,null,r,n):le(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ne(r,l),ys(e,t,r,l,n);case 7:return le(e,t,t.pendingProps,n),t.child;case 8:return le(e,t,t.pendingProps.children,n),t.child;case 12:return le(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,M(tl,r._currentValue),r._currentValue=i,o!==null)if(Me(o.value,i)){if(o.children===l.children&&!ae.current){t=Xe(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var u=o.dependencies;if(u!==null){i=o.child;for(var s=u.firstContext;s!==null;){if(s.context===r){if(o.tag===1){s=Qe(-1,n&-n),s.tag=2;var c=o.updateQueue;if(c!==null){c=c.shared;var m=c.pending;m===null?s.next=s:(s.next=m.next,m.next=s),c.pending=s}}o.lanes|=n,s=o.alternate,s!==null&&(s.lanes|=n),Bo(o.return,n,t),u.lanes|=n;break}s=s.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(h(341));i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Bo(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}le(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,bt(t,n),l=Ce(l),r=r(l),t.flags|=1,le(e,t,r,n),t.child;case 14:return r=t.type,l=Ne(r,t.pendingProps),l=Ne(r.type,l),ws(e,t,r,l,n);case 15:return oc(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Ne(r,l),jr(e,t),t.tag=1,ce(r)?(e=!0,Jr(t)):e=!1,bt(t,n),nc(t,r,l),Qo(t,r,l,n),Go(null,t,r,!0,e,n);case 19:return cc(e,t,n);case 22:return ic(e,t,n)}throw Error(h(156,t.tag))};function Tc(e,t){return bs(e,t)}function fp(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ke(e,t,n,r){return new fp(e,t,n,r)}function Qi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function dp(e){if(typeof e=="function")return Qi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ci)return 11;if(e===fi)return 14}return 2}function dt(e,t){var n=e.alternate;return n===null?(n=ke(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function $r(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Qi(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case jt:return Tt(n.children,l,o,t);case ai:i=8,l|=8;break;case go:return e=ke(12,n,t,l|2),e.elementType=go,e.lanes=o,e;case vo:return e=ke(13,n,t,l),e.elementType=vo,e.lanes=o,e;case ho:return e=ke(19,n,t,l),e.elementType=ho,e.lanes=o,e;case Fs:return El(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ds:i=10;break e;case Is:i=9;break e;case ci:i=11;break e;case fi:i=14;break e;case be:i=16,r=null;break e}throw Error(h(130,e==null?e:typeof e,""))}return t=ke(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function Tt(e,t,n,r){return e=ke(7,e,r,t),e.lanes=n,e}function El(e,t,n,r){return e=ke(22,e,r,t),e.elementType=Fs,e.lanes=n,e.stateNode={isHidden:!1},e}function fo(e,t,n){return e=ke(6,e,null,t),e.lanes=n,e}function po(e,t,n){return t=ke(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function pp(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Xl(0),this.expirationTimes=Xl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Xl(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Ki(e,t,n,r,l,o,i,u,s){return e=new pp(e,t,n,u,s),t===1?(t=1,o===!0&&(t|=8)):t=0,o=ke(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},zi(o),e}function mp(e,t,n){var r=3{"use strict";function Rc(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Rc)}catch(e){console.error(e)}}Rc(),Mc.exports=Lc()});var Ic=Ve(Zi=>{"use strict";var Dc=Oc();Zi.createRoot=Dc.createRoot,Zi.hydrateRoot=Dc.hydrateRoot;var Up});var Ac=Ve(Nl=>{"use strict";var wp=ur(),Sp=Symbol.for("react.element"),kp=Symbol.for("react.fragment"),Ep=Object.prototype.hasOwnProperty,Cp=wp.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,_p={key:!0,ref:!0,__self:!0,__source:!0};function Fc(e,t,n){var r,l={},o=null,i=null;n!==void 0&&(o=""+n),t.key!==void 0&&(o=""+t.key),t.ref!==void 0&&(i=t.ref);for(r in t)Ep.call(t,r)&&!_p.hasOwnProperty(r)&&(l[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)l[r]===void 0&&(l[r]=t[r]);return{$$typeof:Sp,type:e,key:o,ref:i,props:l,_owner:Cp.current}}Nl.Fragment=kp;Nl.jsx=Fc;Nl.jsxs=Fc});var qi=Ve((Wp,jc)=>{"use strict";jc.exports=Ac()});var W=rr(ur(),1),Qc=rr(Ic(),1),x=rr(qi(),1),Uc="openai:set_globals",Vc="mcp-tool-manager-style",Kc="mcp-tool-manager-root",Yc=10,zl={status:"info",activeServers:[],message:"Search the catalog to find MCP servers you can enable."};function xp(){if(typeof document>"u"||document.getElementById(Vc))return;let e=document.createElement("style");e.id=Vc,e.textContent=` + :root { + color-scheme: light dark; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; + } + #${Kc} { + height: 100%; + } + .mcp-tool-manager { + display: flex; + flex-direction: column; + gap: 1rem; + height: 100%; + box-sizing: border-box; + padding: 1.25rem; + background: transparent; + color: var(--mcp-tool-manager-fg, inherit); + } + .mcp-tool-manager header h1 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + } + .mcp-tool-manager header p { + margin: 0.25rem 0 0; + color: var(--mcp-tool-manager-muted, rgba(120, 120, 120, 0.9)); + font-size: 0.95rem; + line-height: 1.4; + } + .mcp-status { + padding: 0.75rem 1rem; + border-radius: 0.75rem; + font-size: 0.95rem; + line-height: 1.3; + } + .mcp-status.success { + background: rgba(16, 185, 129, 0.14); + color: rgba(6, 95, 70, 0.95); + } + .mcp-status.error { + background: rgba(248, 113, 113, 0.18); + color: rgba(153, 27, 27, 0.95); + } + .mcp-status.info { + background: rgba(59, 130, 246, 0.16); + color: rgba(30, 64, 175, 0.95); + } + .mcp-search-card { + border: 1px solid rgba(120, 120, 120, 0.2); + border-radius: 1rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + background: color-mix(in srgb, currentColor 4%, transparent); + box-shadow: 0 2px 8px rgba(15, 15, 15, 0.04); + } + .mcp-search-card form { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .mcp-search-row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + .mcp-search-row input[type="text"] { + flex: 1 1 220px; + padding: 0.55rem 0.75rem; + border-radius: 0.65rem; + border: 1px solid rgba(120, 120, 120, 0.35); + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.04); + color: inherit; + } + .mcp-search-row button { + padding: 0.55rem 1rem; + border-radius: 0.65rem; + border: none; + font-weight: 600; + font-size: 0.95rem; + background: rgba(59, 130, 246, 0.92); + color: #fff; + cursor: pointer; + } + .mcp-search-row button[disabled], + .mcp-action-button[disabled] { + opacity: 0.55; + cursor: not-allowed; + } + .mcp-active-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + .mcp-chip { + padding: 0.35rem 0.65rem; + border-radius: 999px; + font-size: 0.85rem; + background: rgba(59, 130, 246, 0.12); + color: rgba(37, 99, 235, 0.95); + } + .mcp-card-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; + } + .mcp-card { + border: 1px solid rgba(120, 120, 120, 0.18); + border-radius: 1rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + background: color-mix(in srgb, currentColor 3%, transparent); + box-shadow: 0 2px 6px rgba(15, 15, 15, 0.04); + } + .mcp-card h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + .mcp-badge { + font-size: 0.75rem; + font-weight: 600; + padding: 0.2rem 0.45rem; + border-radius: 999px; + background: rgba(16, 185, 129, 0.18); + color: rgba(6, 95, 70, 0.9); + } + .mcp-card p { + margin: 0; + font-size: 0.9rem; + line-height: 1.45; + color: var(--mcp-tool-manager-muted, rgba(120, 120, 120, 0.9)); + } + .mcp-card dl { + margin: 0; + display: grid; + grid-template-columns: auto 1fr; + gap: 0.35rem 0.75rem; + font-size: 0.85rem; + } + .mcp-card dt { + font-weight: 600; + color: rgba(100, 100, 100, 0.9); + } + .mcp-card dd { + margin: 0; + color: inherit; + } + .mcp-card .tool-list { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-top: 0.25rem; + } + .mcp-card .tool-list span { + font-size: 0.8rem; + color: rgba(120, 120, 120, 0.9); + } + .mcp-card-actions { + margin-top: auto; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + .mcp-action-button { + flex: 0 0 auto; + border-radius: 0.6rem; + border: none; + padding: 0.5rem 0.9rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.1s ease; + } + .mcp-action-button.primary { + background: rgba(16, 185, 129, 0.9); + color: white; + } + .mcp-action-button.secondary { + background: rgba(248, 113, 113, 0.9); + color: white; + } + .mcp-empty { + font-size: 0.9rem; + color: var(--mcp-tool-manager-muted, rgba(120, 120, 120, 0.9)); + padding: 0.5rem 0; + } + .mcp-footer { + font-size: 0.75rem; + color: rgba(120, 120, 120, 0.75); + text-align: right; + } + `,document.head.appendChild(e)}function Gc(e){return(0,W.useSyncExternalStore)(t=>{let n=r=>{let l=r;l.detail?.globals&&e in l.detail.globals&&t()};return window.addEventListener(Uc,n,{passive:!0}),()=>window.removeEventListener(Uc,n)},()=>window.openai?.[e],()=>{})}function Tp(){return Gc("toolOutput")}function Np(e){let t=Gc("widgetState"),[n,r]=(0,W.useState)(()=>t??(typeof e=="function"?e():e??null));(0,W.useEffect)(()=>{t!==void 0&&r(t)},[t]);let l=(0,W.useCallback)(o=>{r(i=>{let u=typeof o=="function"?o(i):o;return u!=null&&window.openai?.setWidgetState?.(u),u})},[]);return[n,l]}function eu(e){if(!e||e.length===0)return[];let t=new Set;return e.forEach(n=>{n&&t.add(n)}),Array.from(t).sort((n,r)=>n.localeCompare(r))}function $c(e){if(!e||typeof e!="object")return{name:""};let t=e,n=l=>{if(Array.isArray(l))return l.map(o=>String(o)).filter(Boolean)},r=Array.isArray(t.tools)?t.tools.map(l=>{if(!l||typeof l!="object")return null;let o=l,i=typeof o.name=="string"?o.name:void 0;return i?{name:i,description:typeof o.description=="string"?o.description:void 0}:null}).filter(Boolean):void 0;return{name:typeof t.name=="string"?t.name:"",description:typeof t.description=="string"?t.description:void 0,type:typeof t.type=="string"?t.type:void 0,remoteUrl:typeof t.remoteUrl=="string"?t.remoteUrl:typeof t.remote_url=="string"?t.remote_url:void 0,image:typeof t.image=="string"?t.image:void 0,longLived:typeof t.longLived=="boolean"?t.longLived:typeof t.long_lived=="boolean"?t.long_lived:void 0,requiredSecrets:n(t.requiredSecrets)??n(t.required_secrets)??n(t.requiredSecretsNames),configSchema:t.configSchema??t.config_schema,tools:r,oauthProviders:n(t.oauthProviders)??n(t.oauth_providers)}}function Pp(e){if(!e||typeof e!="object")return;let t=e,n=typeof t.type=="string"?t.type:"",r=n==="add"||n==="remove"||n==="find"?n:"find",o=(typeof t.status=="string"?t.status:"")==="error"?"error":"success";return{type:r,status:o,message:typeof t.message=="string"?t.message:void 0,server:typeof t.server=="string"?t.server:void 0}}function yt(e){if(e?.content){for(let t of e.content)if(typeof t?.text=="string"&&t.text.trim().length>0)return t.text.trim()}}function Ji(e){if(!e)return null;let t=e.structuredContent,n=t&&typeof t=="object"&&!Array.isArray(t)?t:null;if(!n){let c=yt(e)??void 0;return{...zl,status:e.isError?"error":"info",message:c}}let r=typeof n.status=="string"?n.status:void 0,l=Array.isArray(n.results)?n.results.map(c=>$c(c)):void 0,o=eu(Array.isArray(n.activeServers)?n.activeServers:Array.isArray(n.active_servers)?n.active_servers:void 0),i=n.server&&typeof n.server=="object"?$c(n.server):void 0,u=Pp(n.lastAction),s=typeof n.message=="string"?n.message:typeof n.info=="string"?n.info:void 0;return s||(s=yt(e)),{view:typeof n.view=="string"?n.view:void 0,sourceTool:typeof n.sourceTool=="string"?n.sourceTool:void 0,status:r??(e.isError?"error":void 0),message:s,query:typeof n.query=="string"?n.query:void 0,limit:typeof n.limit=="number"?n.limit:void 0,totalMatches:typeof n.totalMatches=="number"?n.totalMatches:void 0,activeServers:o,results:l,server:i,lastAction:u,timestamp:typeof n.timestamp=="string"?n.timestamp:void 0,error:typeof n.error=="string"?n.error:void 0}}function Pl(e){let t=e??zl,n=eu(t.activeServers),r=new Set(n),l=(t.results??[]).map(i=>({...i,isActive:r.has(i.name)})),o=t.server?{...t.server,isActive:r.has(t.server.name)}:void 0;return{...t,activeServers:n,results:l,server:o}}function Wc(e,t,n){let r="find";return e.tool==="mcp-add"?r="add":e.tool==="mcp-remove"&&(r="remove"),{type:r,server:e.server,status:t==="error"?"error":"success",message:n}}function Bc(e,t,n){let r=e??zl;if(!t){let o=n.status??r.status??"info",i=n.message??r.message;return Pl({...r,status:o,message:i,lastAction:Wc(n,o,i)})}let l={...r,...t,activeServers:t.activeServers&&t.activeServers.length>0?eu(t.activeServers):r.activeServers,results:t.results??r.results,query:t.query??r.query,limit:t.limit??r.limit??Yc};return l.status=t.status??n.status??r.status??"info",l.message=t.message??n.message??r.message,t.lastAction||(l.lastAction=Wc(n,l.status??"info",l.message)),Pl(l)}function zp(e){if(!e)return"";try{return JSON.stringify({structuredContent:e.structuredContent??null,isError:!!e.isError,message:yt(e)??null})}catch{return String(Date.now())}}function Lp(e,t,n){if(t.message)return t.message;if(e==="mcp-find"){let r=t.message??n.query;return r?`Showing results for \u201C${r}\u201D.`:"Showing search results."}return e==="mcp-add"?t.server?`Added \u201C${t.server}\u201D.`:"Server added.":e==="mcp-remove"?t.server?`Removed \u201C${t.server}\u201D.`:"Server removed.":"Operation completed."}function bi(e){return e instanceof Error?e.message:typeof e=="string"?e:"Something went wrong. Please try again."}var Rp=()=>{let e=Tp(),t=(0,W.useMemo)(()=>zp(e),[e]),[n,r]=Np(()=>Ji(window.openai?.toolOutput)??zl),l=(0,W.useMemo)(()=>Pl(n),[n]),[o,i]=(0,W.useState)(()=>l.query??""),[u,s]=(0,W.useState)(null),[c,m]=(0,W.useState)(null),[v,g]=(0,W.useState)(!1);(0,W.useEffect)(()=>{l.query!==void 0&&i(l.query)},[l.query]),(0,W.useEffect)(()=>{if(!e)return;let p=Ji(e),E={tool:p?.sourceTool==="mcp-add"||p?.sourceTool==="mcp-remove"||p?.sourceTool==="mcp-find"?p.sourceTool:"mcp-find",server:p?.server?.name??p?.lastAction?.server,message:p?.message??yt(e)??void 0,status:p?.status??(e.isError?"error":void 0)};r(_=>Bc(_,p,E))},[r,e,t]);let w=(0,W.useCallback)(async(p,y,E)=>{if(!window.openai?.callTool)throw new Error("Tool calling is unavailable in this context.");let _=await window.openai.callTool(p,y),T=Ji(_),B=T?.status??(_.isError?"error":E.status??"success"),P=Pl(n),Oe=T?.message??yt(_)??Lp(p,E,P);return r(Ll=>Bc(Ll,T,{...E,tool:p,message:Oe,status:B})),_},[r,n]),S=(0,W.useCallback)(async p=>{p?.preventDefault();let y=o.trim();if(!y){m("Enter a name, description, or keyword to search the catalog.");return}m(null),g(!0);try{let E=await w("mcp-find",{query:y,limit:l.limit??Yc},{tool:"mcp-find",message:`Showing results for \u201C${y}\u201D.`,status:"success"});E.isError&&m(yt(E)??"The search tool returned an error.")}catch(E){m(bi(E))}finally{g(!1)}},[l.limit,w,o]),C=(0,W.useCallback)(async p=>{m(null),s(`add:${p}`);try{let y=await w("mcp-add",{name:p},{tool:"mcp-add",server:p,message:`Added \u201C${p}\u201D.`,status:"success"});y.isError&&m(yt(y)??`Failed to add \u201C${p}\u201D.`)}catch(y){m(bi(y))}finally{s(null)}},[w]),F=(0,W.useCallback)(async p=>{m(null),s(`remove:${p}`);try{let y=await w("mcp-remove",{name:p},{tool:"mcp-remove",server:p,message:`Removed \u201C${p}\u201D.`,status:"info"});y.isError&&m(yt(y)??`Failed to remove \u201C${p}\u201D.`)}catch(y){m(bi(y))}finally{s(null)}},[w]),f=l.activeServers,a=l.results??[],d=l.status==="error"?"error":l.status==="success"?"success":"info";return(0,x.jsxs)("div",{className:"mcp-tool-manager",role:"application",children:[(0,x.jsxs)("header",{children:[(0,x.jsx)("h1",{children:"MCP Tool Manager"}),(0,x.jsx)("p",{children:"Search the catalog, add servers you need, and disable ones you no longer use."})]}),(l.message||c)&&(0,x.jsx)("div",{className:`mcp-status ${c?"error":d}`,role:"status",children:c??l.message}),(0,x.jsxs)("section",{className:"mcp-search-card","aria-label":"Search catalog",children:[(0,x.jsxs)("form",{onSubmit:S,children:[(0,x.jsx)("label",{htmlFor:"mcp-search-input",children:"Search catalog servers"}),(0,x.jsxs)("div",{className:"mcp-search-row",children:[(0,x.jsx)("input",{id:"mcp-search-input",type:"text",placeholder:"Search by server name, capability, or description",value:o,onChange:p=>i(p.target.value),autoComplete:"off",disabled:v}),(0,x.jsx)("button",{type:"submit",disabled:v,children:v?"Searching\u2026":"Search"})]})]}),(0,x.jsxs)("div",{children:[(0,x.jsx)("strong",{children:"Active servers:"}),f.length===0?(0,x.jsx)("span",{className:"mcp-empty",children:" None enabled yet."}):(0,x.jsx)("div",{className:"mcp-active-list","aria-live":"polite",children:f.map(p=>(0,x.jsx)("span",{className:"mcp-chip",children:p},p))})]})]}),(0,x.jsx)("section",{"aria-label":"Search results",children:a.length===0?(0,x.jsx)("div",{className:"mcp-empty",children:l.query?`No servers matched \u201C${l.query}\u201D. Try a different keyword.`:"Start by searching to see available servers you can enable."}):(0,x.jsx)("div",{className:"mcp-card-list",children:a.map(p=>{let y=u===`add:${p.name}`,E=u===`remove:${p.name}`;return(0,x.jsxs)("article",{className:"mcp-card","aria-live":"polite",children:[(0,x.jsxs)("h2",{children:[p.name,p.isActive&&(0,x.jsx)("span",{className:"mcp-badge",children:"Active"})]}),(0,x.jsx)("p",{children:p.description??"No description provided in the catalog."}),(0,x.jsxs)("dl",{children:[p.type&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)("dt",{children:"Type"}),(0,x.jsx)("dd",{children:p.type})]}),p.remoteUrl&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)("dt",{children:"Endpoint"}),(0,x.jsx)("dd",{children:p.remoteUrl})]}),p.image&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)("dt",{children:"Image"}),(0,x.jsx)("dd",{children:p.image})]}),typeof p.longLived=="boolean"&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)("dt",{children:"Lifecycle"}),(0,x.jsx)("dd",{children:p.longLived?"Long-lived":"Ephemeral"})]}),p.requiredSecrets&&p.requiredSecrets.length>0&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)("dt",{children:"Secrets"}),(0,x.jsx)("dd",{children:p.requiredSecrets.join(", ")})]})]}),p.tools&&p.tools.length>0&&(0,x.jsxs)("div",{className:"tool-list",children:[(0,x.jsx)("strong",{children:"Tools"}),p.tools.slice(0,3).map(_=>(0,x.jsxs)("span",{children:[_.name,_.description?` \u2014 ${_.description}`:""]},_.name)),p.tools.length>3&&(0,x.jsxs)("span",{children:["+",p.tools.length-3," more tools"]})]}),(0,x.jsxs)("div",{className:"mcp-card-actions",children:[(0,x.jsx)("button",{type:"button",className:"mcp-action-button primary",onClick:()=>C(p.name),disabled:p.isActive||u!==null,children:y?"Adding\u2026":p.isActive?"Already active":"Add server"}),(0,x.jsx)("button",{type:"button",className:"mcp-action-button secondary",onClick:()=>F(p.name),disabled:!p.isActive||u!==null,children:E?"Removing\u2026":"Remove server"})]})]},p.name)})})}),(0,x.jsx)("footer",{className:"mcp-footer","aria-live":"polite",children:l.timestamp?`Last updated ${new Date(l.timestamp).toLocaleString()}`:null})]})};function Hc(){xp();let e=document.getElementById(Kc);if(!e){console.error("MCP Tool Manager UI failed to mount: root element not found.");return}if(e.dataset.mounted==="true")return;e.dataset.mounted="true",(0,Qc.createRoot)(e).render((0,x.jsx)(Rp,{}))}typeof window<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>Hc(),{once:!0}):Hc()); +/*! Bundled license information: + +react/cjs/react.production.min.js: + (** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +scheduler/cjs/scheduler.production.min.js: + (** + * @license React + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +react-dom/cjs/react-dom.production.min.js: + (** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +react/cjs/react-jsx-runtime.production.min.js: + (** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) +*/ +//# sourceMappingURL=tool-manager.js.map diff --git a/ui/tool-manager/package-lock.json b/ui/tool-manager/package-lock.json new file mode 100644 index 00000000..11253cdd --- /dev/null +++ b/ui/tool-manager/package-lock.json @@ -0,0 +1,552 @@ +{ + "name": "mcp-tool-manager-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-tool-manager-ui", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "esbuild": "^0.24.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/ui/tool-manager/package.json b/ui/tool-manager/package.json new file mode 100644 index 00000000..672ee6c3 --- /dev/null +++ b/ui/tool-manager/package.json @@ -0,0 +1,18 @@ +{ + "name": "mcp-tool-manager-ui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.tsx --bundle --format=esm --target=es2020 --jsx=automatic --minify --outfile=dist/tool-manager.js --log-level=info --sourcemap && mkdir -p ../../pkg/gateway/ui/embedded && cp dist/tool-manager.js ../../pkg/gateway/ui/embedded/tool-manager.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "esbuild": "^0.24.0", + "typescript": "^5.6.3" + } +} diff --git a/ui/tool-manager/src/index.tsx b/ui/tool-manager/src/index.tsx new file mode 100644 index 00000000..5ad5a4e1 --- /dev/null +++ b/ui/tool-manager/src/index.tsx @@ -0,0 +1,956 @@ +import React, { + useCallback, + useEffect, + useMemo, + useState, + useSyncExternalStore, + type SetStateAction, +} from "react"; +import { createRoot } from "react-dom/client"; + +type ToolName = "mcp-find" | "mcp-add" | "mcp-remove"; +type WidgetStatus = "success" | "error" | "info"; + +type ToolContent = { + type?: string; + text?: string; + [key: string]: unknown; +}; + +type CallToolResponse = { + content?: ToolContent[]; + structuredContent?: unknown; + isError?: boolean; + _meta?: Record; +}; + +type ToolManagerAction = { + type: "find" | "add" | "remove"; + status: "success" | "error"; + message?: string; + server?: string; +}; + +type ToolManagerServer = { + name: string; + description?: string; + type?: string; + remoteUrl?: string; + image?: string; + longLived?: boolean; + requiredSecrets?: string[]; + configSchema?: unknown; + tools?: { name: string; description?: string }[]; + oauthProviders?: string[]; + isActive?: boolean; +}; + +type ToolManagerState = { + view?: string; + sourceTool?: string; + status?: WidgetStatus; + message?: string; + query?: string; + limit?: number; + totalMatches?: number; + activeServers: string[]; + results?: ToolManagerServer[]; + server?: ToolManagerServer; + lastAction?: ToolManagerAction; + timestamp?: string; + error?: string; +}; + +type OpenAiGlobals = { + toolOutput?: CallToolResponse | null; + widgetState?: ToolManagerState | null; + theme?: "light" | "dark"; + displayMode?: "inline" | "fullscreen" | "pip"; + maxHeight?: number; + locale?: string; + userAgent?: { device?: { type?: string } }; + safeArea?: { insets: { top: number; right: number; bottom: number; left: number } }; +}; + +type OpenAiAPI = { + callTool?: (name: string, args: Record) => Promise; + sendFollowUpMessage?: (args: { prompt: string }) => Promise; + openExternal?: (payload: { href: string }) => void; + requestDisplayMode?: (args: { mode: "pip" | "inline" | "fullscreen" }) => Promise<{ mode: string }>; + setWidgetState?: (state: unknown) => Promise; + setLayout?: (layout: unknown) => Promise; +}; + +type OpenAiHost = OpenAiAPI & OpenAiGlobals; + +declare global { + interface Window { + openai?: OpenAiHost; + } + + interface WindowEventMap { + [SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent; + } +} + +type SetGlobalsEvent = CustomEvent<{ + globals: Partial; +}>; + +const SET_GLOBALS_EVENT_TYPE = "openai:set_globals"; +const STYLE_TAG_ID = "mcp-tool-manager-style"; +const ROOT_ELEMENT_ID = "mcp-tool-manager-root"; +const DEFAULT_LIMIT = 10; + +const EMPTY_STATE: ToolManagerState = { + status: "info", + activeServers: [], + message: "Search the catalog to find MCP servers you can enable.", +}; + +function injectStyles(): void { + if (typeof document === "undefined") { + return; + } + if (document.getElementById(STYLE_TAG_ID)) { + return; + } + const style = document.createElement("style"); + style.id = STYLE_TAG_ID; + style.textContent = ` + :root { + color-scheme: light dark; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; + } + #${ROOT_ELEMENT_ID} { + height: 100%; + } + .mcp-tool-manager { + display: flex; + flex-direction: column; + gap: 1rem; + height: 100%; + box-sizing: border-box; + padding: 1.25rem; + background: transparent; + color: var(--mcp-tool-manager-fg, inherit); + } + .mcp-tool-manager header h1 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + } + .mcp-tool-manager header p { + margin: 0.25rem 0 0; + color: var(--mcp-tool-manager-muted, rgba(120, 120, 120, 0.9)); + font-size: 0.95rem; + line-height: 1.4; + } + .mcp-status { + padding: 0.75rem 1rem; + border-radius: 0.75rem; + font-size: 0.95rem; + line-height: 1.3; + } + .mcp-status.success { + background: rgba(16, 185, 129, 0.14); + color: rgba(6, 95, 70, 0.95); + } + .mcp-status.error { + background: rgba(248, 113, 113, 0.18); + color: rgba(153, 27, 27, 0.95); + } + .mcp-status.info { + background: rgba(59, 130, 246, 0.16); + color: rgba(30, 64, 175, 0.95); + } + .mcp-search-card { + border: 1px solid rgba(120, 120, 120, 0.2); + border-radius: 1rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + background: color-mix(in srgb, currentColor 4%, transparent); + box-shadow: 0 2px 8px rgba(15, 15, 15, 0.04); + } + .mcp-search-card form { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .mcp-search-row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + .mcp-search-row input[type="text"] { + flex: 1 1 220px; + padding: 0.55rem 0.75rem; + border-radius: 0.65rem; + border: 1px solid rgba(120, 120, 120, 0.35); + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.04); + color: inherit; + } + .mcp-search-row button { + padding: 0.55rem 1rem; + border-radius: 0.65rem; + border: none; + font-weight: 600; + font-size: 0.95rem; + background: rgba(59, 130, 246, 0.92); + color: #fff; + cursor: pointer; + } + .mcp-search-row button[disabled], + .mcp-action-button[disabled] { + opacity: 0.55; + cursor: not-allowed; + } + .mcp-active-list { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } + .mcp-chip { + padding: 0.35rem 0.65rem; + border-radius: 999px; + font-size: 0.85rem; + background: rgba(59, 130, 246, 0.12); + color: rgba(37, 99, 235, 0.95); + } + .mcp-card-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1rem; + } + .mcp-card { + border: 1px solid rgba(120, 120, 120, 0.18); + border-radius: 1rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + background: color-mix(in srgb, currentColor 3%, transparent); + box-shadow: 0 2px 6px rgba(15, 15, 15, 0.04); + } + .mcp-card h2 { + margin: 0; + font-size: 1.05rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + .mcp-badge { + font-size: 0.75rem; + font-weight: 600; + padding: 0.2rem 0.45rem; + border-radius: 999px; + background: rgba(16, 185, 129, 0.18); + color: rgba(6, 95, 70, 0.9); + } + .mcp-card p { + margin: 0; + font-size: 0.9rem; + line-height: 1.45; + color: var(--mcp-tool-manager-muted, rgba(120, 120, 120, 0.9)); + } + .mcp-card dl { + margin: 0; + display: grid; + grid-template-columns: auto 1fr; + gap: 0.35rem 0.75rem; + font-size: 0.85rem; + } + .mcp-card dt { + font-weight: 600; + color: rgba(100, 100, 100, 0.9); + } + .mcp-card dd { + margin: 0; + color: inherit; + } + .mcp-card .tool-list { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-top: 0.25rem; + } + .mcp-card .tool-list span { + font-size: 0.8rem; + color: rgba(120, 120, 120, 0.9); + } + .mcp-card-actions { + margin-top: auto; + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + .mcp-action-button { + flex: 0 0 auto; + border-radius: 0.6rem; + border: none; + padding: 0.5rem 0.9rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.1s ease; + } + .mcp-action-button.primary { + background: rgba(16, 185, 129, 0.9); + color: white; + } + .mcp-action-button.secondary { + background: rgba(248, 113, 113, 0.9); + color: white; + } + .mcp-empty { + font-size: 0.9rem; + color: var(--mcp-tool-manager-muted, rgba(120, 120, 120, 0.9)); + padding: 0.5rem 0; + } + .mcp-footer { + font-size: 0.75rem; + color: rgba(120, 120, 120, 0.75); + text-align: right; + } + `; + document.head.appendChild(style); +} + +function useOpenAiGlobal(key: K): OpenAiGlobals[K] | undefined { + return useSyncExternalStore( + (onChange) => { + const handler = (event: Event): void => { + const custom = event as SetGlobalsEvent; + if (custom.detail?.globals && key in custom.detail.globals) { + onChange(); + } + }; + window.addEventListener(SET_GLOBALS_EVENT_TYPE, handler, { passive: true }); + return () => window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handler); + }, + () => window.openai?.[key], + () => undefined, + ); +} + +function useToolOutput(): CallToolResponse | null | undefined { + return useOpenAiGlobal("toolOutput"); +} + +function useWidgetState( + defaultState?: T | (() => T | null) | null, +): readonly [T | null, (state: SetStateAction) => void] { + const widgetStateFromWindow = useOpenAiGlobal("widgetState") as T | null | undefined; + const [widgetState, setWidgetStateInternal] = useState(() => { + if (widgetStateFromWindow != null) { + return widgetStateFromWindow; + } + if (typeof defaultState === "function") { + return (defaultState as () => T | null)(); + } + return defaultState ?? null; + }); + + useEffect(() => { + if (widgetStateFromWindow !== undefined) { + setWidgetStateInternal(widgetStateFromWindow); + } + }, [widgetStateFromWindow]); + + const setWidgetState = useCallback( + (value: SetStateAction) => { + setWidgetStateInternal((prev) => { + const next = typeof value === "function" ? (value as (prevState: T | null) => T | null)(prev) : value; + if (next != null) { + void window.openai?.setWidgetState?.(next); + } + return next; + }); + }, + [], + ); + + return [widgetState, setWidgetState] as const; +} + +function dedupeStrings(values: string[] | undefined): string[] { + if (!values || values.length === 0) { + return []; + } + const seen = new Set(); + values.forEach((value) => { + if (value) { + seen.add(value); + } + }); + return Array.from(seen).sort((a, b) => a.localeCompare(b)); +} + +function normalizeServer(value: unknown): ToolManagerServer { + if (!value || typeof value !== "object") { + return { name: "" }; + } + const data = value as Record; + const toStringArray = (input: unknown): string[] | undefined => { + if (!Array.isArray(input)) { + return undefined; + } + return input.map((item) => String(item)).filter(Boolean); + }; + const tools = Array.isArray(data.tools) + ? data.tools + .map((tool) => { + if (!tool || typeof tool !== "object") { + return null; + } + const record = tool as Record; + const name = typeof record.name === "string" ? record.name : undefined; + if (!name) { + return null; + } + return { + name, + description: typeof record.description === "string" ? record.description : undefined, + }; + }) + .filter(Boolean) as { name: string; description?: string }[] + : undefined; + + return { + name: typeof data.name === "string" ? data.name : "", + description: typeof data.description === "string" ? data.description : undefined, + type: typeof data.type === "string" ? data.type : undefined, + remoteUrl: + typeof (data.remoteUrl as string) === "string" + ? (data.remoteUrl as string) + : typeof (data.remote_url as string) === "string" + ? (data.remote_url as string) + : undefined, + image: typeof data.image === "string" ? data.image : undefined, + longLived: + typeof (data.longLived as boolean) === "boolean" + ? (data.longLived as boolean) + : typeof (data.long_lived as boolean) === "boolean" + ? (data.long_lived as boolean) + : undefined, + requiredSecrets: + toStringArray(data.requiredSecrets) ?? + toStringArray(data.required_secrets) ?? + toStringArray(data.requiredSecretsNames), + configSchema: data.configSchema ?? data.config_schema, + tools, + oauthProviders: + toStringArray(data.oauthProviders) ?? toStringArray((data.oauth_providers as string[] | undefined)), + }; +} + +function normalizeAction(value: unknown): ToolManagerAction | undefined { + if (!value || typeof value !== "object") { + return undefined; + } + const data = value as Record; + const rawType = typeof data.type === "string" ? data.type : ""; + const type: ToolManagerAction["type"] = + rawType === "add" || rawType === "remove" || rawType === "find" ? rawType : "find"; + const rawStatus = typeof data.status === "string" ? data.status : ""; + const status: ToolManagerAction["status"] = rawStatus === "error" ? "error" : "success"; + return { + type, + status, + message: typeof data.message === "string" ? data.message : undefined, + server: typeof data.server === "string" ? data.server : undefined, + }; +} + +function extractFirstText(response: CallToolResponse | null | undefined): string | undefined { + if (!response?.content) { + return undefined; + } + for (const item of response.content) { + if (typeof item?.text === "string" && item.text.trim().length > 0) { + return item.text.trim(); + } + } + return undefined; +} + +function parseCallToolResponse(response: CallToolResponse | null | undefined): ToolManagerState | null { + if (!response) { + return null; + } + + const structured = response.structuredContent; + const base = + structured && typeof structured === "object" && !Array.isArray(structured) + ? (structured as Record) + : null; + + if (!base) { + const message = extractFirstText(response) ?? undefined; + return { + ...EMPTY_STATE, + status: response.isError ? "error" : "info", + message, + }; + } + + const status = typeof base.status === "string" ? (base.status as WidgetStatus) : undefined; + const results = Array.isArray(base.results) + ? (base.results.map((item) => normalizeServer(item)) as ToolManagerServer[]) + : undefined; + + const activeServers = dedupeStrings( + Array.isArray(base.activeServers) + ? (base.activeServers as string[]) + : Array.isArray(base.active_servers) + ? (base.active_servers as string[]) + : undefined, + ); + + const server = + base.server && typeof base.server === "object" ? normalizeServer(base.server as Record) : undefined; + + const lastAction = normalizeAction(base.lastAction); + + let message: string | undefined = + typeof base.message === "string" ? base.message : typeof base.info === "string" ? base.info : undefined; + if (!message) { + message = extractFirstText(response); + } + + return { + view: typeof base.view === "string" ? base.view : undefined, + sourceTool: typeof base.sourceTool === "string" ? base.sourceTool : undefined, + status: status ?? (response.isError ? "error" : undefined), + message, + query: typeof base.query === "string" ? base.query : undefined, + limit: typeof base.limit === "number" ? base.limit : undefined, + totalMatches: typeof base.totalMatches === "number" ? base.totalMatches : undefined, + activeServers, + results, + server, + lastAction, + timestamp: typeof base.timestamp === "string" ? base.timestamp : undefined, + error: typeof base.error === "string" ? base.error : undefined, + }; +} + +function decorateState(state: ToolManagerState | null): ToolManagerState { + const base = state ?? EMPTY_STATE; + const activeServers = dedupeStrings(base.activeServers); + const activeSet = new Set(activeServers); + const results = (base.results ?? []).map((server) => ({ + ...server, + isActive: activeSet.has(server.name), + })); + const server = base.server ? { ...base.server, isActive: activeSet.has(base.server.name) } : undefined; + return { + ...base, + activeServers, + results, + server, + }; +} + +type MergeContext = { + tool: ToolName; + server?: string; + message?: string; + status?: WidgetStatus; +}; + +function actionForContext(ctx: MergeContext, status: WidgetStatus, message?: string): ToolManagerAction { + let type: ToolManagerAction["type"] = "find"; + if (ctx.tool === "mcp-add") { + type = "add"; + } else if (ctx.tool === "mcp-remove") { + type = "remove"; + } + return { + type, + server: ctx.server, + status: status === "error" ? "error" : "success", + message, + }; +} + +function mergeState( + prev: ToolManagerState | null, + next: ToolManagerState | null, + ctx: MergeContext, +): ToolManagerState { + const base = prev ?? EMPTY_STATE; + if (!next) { + const status = ctx.status ?? base.status ?? "info"; + const message = ctx.message ?? base.message; + return decorateState({ + ...base, + status, + message, + lastAction: actionForContext(ctx, status, message), + }); + } + + const merged: ToolManagerState = { + ...base, + ...next, + activeServers: + next.activeServers && next.activeServers.length > 0 ? dedupeStrings(next.activeServers) : base.activeServers, + results: next.results ?? base.results, + query: next.query ?? base.query, + limit: next.limit ?? base.limit ?? DEFAULT_LIMIT, + }; + + merged.status = next.status ?? ctx.status ?? base.status ?? "info"; + merged.message = next.message ?? ctx.message ?? base.message; + + if (!next.lastAction) { + merged.lastAction = actionForContext(ctx, merged.status ?? "info", merged.message); + } + + return decorateState(merged); +} + +function toolOutputSignature(result: CallToolResponse | null | undefined): string { + if (!result) { + return ""; + } + try { + return JSON.stringify({ + structuredContent: result.structuredContent ?? null, + isError: Boolean(result.isError), + message: extractFirstText(result) ?? null, + }); + } catch { + return String(Date.now()); + } +} + +function formatFallbackMessage(tool: ToolName, ctx: MergeContext, state: ToolManagerState): string { + if (ctx.message) { + return ctx.message; + } + if (tool === "mcp-find") { + const term = ctx.message ?? state.query; + return term ? `Showing results for “${term}”.` : "Showing search results."; + } + if (tool === "mcp-add") { + return ctx.server ? `Added “${ctx.server}”.` : "Server added."; + } + if (tool === "mcp-remove") { + return ctx.server ? `Removed “${ctx.server}”.` : "Server removed."; + } + return "Operation completed."; +} + +function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + return "Something went wrong. Please try again."; +} + +const App: React.FC = () => { + const toolOutput = useToolOutput(); + const outputSignature = useMemo(() => toolOutputSignature(toolOutput), [toolOutput]); + const [state, setState] = useWidgetState(() => { + const parsed = parseCallToolResponse(window.openai?.toolOutput); + return parsed ?? EMPTY_STATE; + }); + + const decorated = useMemo(() => decorateState(state), [state]); + const [query, setQuery] = useState(() => decorated.query ?? ""); + const [pendingAction, setPendingAction] = useState(null); + const [inlineError, setInlineError] = useState(null); + const [isSearching, setIsSearching] = useState(false); + + useEffect(() => { + if (decorated.query !== undefined) { + setQuery(decorated.query); + } + }, [decorated.query]); + + useEffect(() => { + if (!toolOutput) { + return; + } + const parsed = parseCallToolResponse(toolOutput); + const sourceTool = + parsed?.sourceTool === "mcp-add" || parsed?.sourceTool === "mcp-remove" || parsed?.sourceTool === "mcp-find" + ? (parsed.sourceTool as ToolName) + : ("mcp-find" satisfies ToolName); + const ctx: MergeContext = { + tool: sourceTool, + server: parsed?.server?.name ?? parsed?.lastAction?.server, + message: parsed?.message ?? extractFirstText(toolOutput) ?? undefined, + status: parsed?.status ?? (toolOutput.isError ? "error" : undefined), + }; + setState((prev) => mergeState(prev, parsed, ctx)); + }, [setState, toolOutput, outputSignature]); + + const invokeTool = useCallback( + async (tool: ToolName, args: Record, ctx: MergeContext) => { + if (!window.openai?.callTool) { + throw new Error("Tool calling is unavailable in this context."); + } + const response = await window.openai.callTool(tool, args); + const parsed = parseCallToolResponse(response); + const status: WidgetStatus = + parsed?.status ?? (response.isError ? "error" : ctx.status ?? "success"); + const baseState = decorateState(state); + const message = parsed?.message ?? extractFirstText(response) ?? formatFallbackMessage(tool, ctx, baseState); + setState((prev) => mergeState(prev, parsed, { ...ctx, tool, message, status })); + return response; + }, + [setState, state], + ); + + const handleSearch = useCallback( + async (event?: React.FormEvent) => { + event?.preventDefault(); + const term = query.trim(); + if (!term) { + setInlineError("Enter a name, description, or keyword to search the catalog."); + return; + } + setInlineError(null); + setIsSearching(true); + try { + const response = await invokeTool( + "mcp-find", + { query: term, limit: decorated.limit ?? DEFAULT_LIMIT }, + { tool: "mcp-find", message: `Showing results for “${term}”.`, status: "success" }, + ); + if (response.isError) { + setInlineError(extractFirstText(response) ?? "The search tool returned an error."); + } + } catch (error) { + setInlineError(formatError(error)); + } finally { + setIsSearching(false); + } + }, + [decorated.limit, invokeTool, query], + ); + + const handleAdd = useCallback( + async (server: string) => { + setInlineError(null); + setPendingAction(`add:${server}`); + try { + const response = await invokeTool( + "mcp-add", + { name: server }, + { tool: "mcp-add", server, message: `Added “${server}”.`, status: "success" }, + ); + if (response.isError) { + setInlineError(extractFirstText(response) ?? `Failed to add “${server}”.`); + } + } catch (error) { + setInlineError(formatError(error)); + } finally { + setPendingAction(null); + } + }, + [invokeTool], + ); + + const handleRemove = useCallback( + async (server: string) => { + setInlineError(null); + setPendingAction(`remove:${server}`); + try { + const response = await invokeTool( + "mcp-remove", + { name: server }, + { tool: "mcp-remove", server, message: `Removed “${server}”.`, status: "info" }, + ); + if (response.isError) { + setInlineError(extractFirstText(response) ?? `Failed to remove “${server}”.`); + } + } catch (error) { + setInlineError(formatError(error)); + } finally { + setPendingAction(null); + } + }, + [invokeTool], + ); + + const activeServers = decorated.activeServers; + const results = decorated.results ?? []; + const statusClass = + decorated.status === "error" ? "error" : decorated.status === "success" ? "success" : "info"; + + return ( +
+
+

MCP Tool Manager

+

Search the catalog, add servers you need, and disable ones you no longer use.

+
+ + {(decorated.message || inlineError) && ( +
+ {inlineError ?? decorated.message} +
+ )} + +
+
+ +
+ setQuery(event.target.value)} + autoComplete="off" + disabled={isSearching} + /> + +
+
+
+ Active servers: + {activeServers.length === 0 ? ( + None enabled yet. + ) : ( +
+ {activeServers.map((name) => ( + + {name} + + ))} +
+ )} +
+
+ +
+ {results.length === 0 ? ( +
+ {decorated.query + ? `No servers matched “${decorated.query}”. Try a different keyword.` + : "Start by searching to see available servers you can enable."} +
+ ) : ( +
+ {results.map((server) => { + const isAddPending = pendingAction === `add:${server.name}`; + const isRemovePending = pendingAction === `remove:${server.name}`; + return ( +
+

+ {server.name} + {server.isActive && Active} +

+

{server.description ?? "No description provided in the catalog."}

+
+ {server.type && ( + <> +
Type
+
{server.type}
+ + )} + {server.remoteUrl && ( + <> +
Endpoint
+
{server.remoteUrl}
+ + )} + {server.image && ( + <> +
Image
+
{server.image}
+ + )} + {typeof server.longLived === "boolean" && ( + <> +
Lifecycle
+
{server.longLived ? "Long-lived" : "Ephemeral"}
+ + )} + {server.requiredSecrets && server.requiredSecrets.length > 0 && ( + <> +
Secrets
+
{server.requiredSecrets.join(", ")}
+ + )} +
+ {server.tools && server.tools.length > 0 && ( +
+ Tools + {server.tools.slice(0, 3).map((tool) => ( + + {tool.name} + {tool.description ? ` — ${tool.description}` : ""} + + ))} + {server.tools.length > 3 && +{server.tools.length - 3} more tools} +
+ )} +
+ + +
+
+ ); + })} +
+ )} +
+ +
+ {decorated.timestamp ? `Last updated ${new Date(decorated.timestamp).toLocaleString()}` : null} +
+
+ ); +}; + +function mount(): void { + injectStyles(); + const container = document.getElementById(ROOT_ELEMENT_ID); + if (!container) { + console.error("MCP Tool Manager UI failed to mount: root element not found."); + return; + } + if (container.dataset.mounted === "true") { + return; + } + container.dataset.mounted = "true"; + const root = createRoot(container); + root.render(); +} + +if (typeof window !== "undefined") { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => mount(), { once: true }); + } else { + mount(); + } +} diff --git a/ui/tool-manager/tsconfig.json b/ui/tool-manager/tsconfig.json new file mode 100644 index 00000000..ac37e4ee --- /dev/null +++ b/ui/tool-manager/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "allowJs": false, + "forceConsistentCasingInFileNames": true, + "types": ["dom", "dom.iterable", "es2020"] + }, + "include": ["src"] +}