MCP (#1981)
Browse files* Update ServerCard.svelte
feat(mcp): add Model Context Protocol integration with tool support
Improve MCP tools caching and server selection logic
Replaces single-entry MCP tools cache with a multi-keyed cache based on server configuration, improving cache accuracy for different server sets. Refactors server selection logic in runMcpFlow to better handle custom server lists and selected server names. Removes MCP flow from generate.ts, streamlining text generation logic.
* Add OAuth authentication for MCP servers
Introduces browser-based OAuth flow for MCP servers, including new auth helpers, localStorage management, and UI actions for authentication and sign-out. Server cards now support authentication, and enabled servers include persisted auth headers for API requests. Adds callback handler and route for OAuth completion.
* Add server authentication status and UI indicators
Introduces an 'authRequired' property to MCPServer and health check responses to indicate when authentication is needed. Updates ServerCard UI to show authentication badges and conditionally display Authenticate/Sign out buttons. Store now tracks authenticated server IDs and updates them on auth changes. Health check logic and error handling improved to detect and signal authentication requirements.
* Show MCP Servers button only for authenticated users
The MCP Servers button in NavMenu is now conditionally rendered for users with a username or email, ensuring only authenticated users can access server management. Also, MCPServerManager modal width and help text background color have been updated for improved UI consistency.
* format
* Refactor MCP client management and tool invocation
Introduces a pooled MCP client system for efficient reuse and connection management. Refactors tool invocation to support per-call timeouts, abort signals, and parallel execution with improved error handling. Ensures MCP clients are properly closed after each flow and updates related modules to use the new client pool and signal-aware APIs.
* Improve OAuth state generation and error handling
Replaces Math.random-based OAuth state with a cryptographically-strong random generator using Web Crypto API. Sanitizes error messages in the callback handler to prevent unsafe HTML embedding. Ensures token_type is trimmed and defaults to 'Bearer' in authentication headers.
* Update blue color shades and improve MCP server UI
Standardizes blue color usage across components for consistency, switching from blue-500 to blue-600 in buttons, borders, and backgrounds. Refactors MCPServerManager and ServerCard to use a Switch component for enabling servers, improving accessibility and code clarity. Increases loop count in runMcpFlow.ts for more robust streaming. Updates multimodal model indicators to use blue-600 in dark mode for better visual alignment.
* Adjust input padding and card border styling
Reduced horizontal padding on AddServerForm input fields for improved layout. Updated ServerCard selected state to use semi-transparent blue border for better visual distinction.
* Improve provider metadata handling in text generation
Sanitizes tool/function names to comply with provider guidelines by replacing disallowed characters and limiting length. Enhances router metadata emission to support cases where only provider information is available, and captures the x-inference-provider header from upstream OpenAI-compatible servers to notify the UI of the provider used.
* Update ServerCard.svelte
* Add per-model tool calling support and overrides
Introduces a `supportsTools` flag for models to indicate tool/function calling capability, aggregates provider support, and adds per-model user overrides via settings. Updates UI to display tool support, enables forced tool calling, and ensures backend respects these settings during text generation and conversation flows.
* lint
* Enhance MCP client to append structuredContent
Updates the MCP HTTP client to append structuredContent as JSON to the textual output if provided by the server. Also bumps @modelcontextprotocol/sdk dependency to version 1.21.1.
* Refactor settings bindings for Svelte 5 compatibility
Replaces direct store mutations with functional bindings for settings fields in model and application settings pages, ensuring proper updates via store methods. Also updates ToolUpdate.svelte to use $derived.by for availableTools and initializes state variables with explicit undefined values.
* Update MCPServerManager.svelte
* Improve MCP server management UI and validation
Added a security warning to the AddServerForm and improved form styling. Updated MCPServerManager to clarify views, use a larger add icon, and show quick tips. ServerCard header styling was refined. MCP server store now prunes invalid selected server IDs after refresh.
* Add router tool support configuration
Introduces LLM_ROUTER_ENABLE_TOOLS environment variable and configures its usage in both .env and prod.yaml. Updates model processing to enable tool support for the router when the variable is set to true.
* Improve MCP server manager and card UI
* Refactor MCP icon and update app name usage
Extracted the MCP icon into a reusable IconMCP.svelte component and replaced inline SVG usage in MCPServerManager.svelte. Updated references to 'HuggingChat' to use the dynamic PUBLIC_APP_NAME from public config for better branding flexibility.
* Update model badges to use colored backgrounds
Replaces border-based styling for tool and multimodal badges with colored backgrounds and text for improved visual clarity in models and settings pages.
* Remove 'Base' label from ServerCard
Eliminates the conditional rendering of the 'Base' label for servers of type 'base' in the ServerCard component.
* Add router bypass for tool-capable models
Implements logic to bypass Arch routing and select a configured tool-capable model when tools are enabled and active, using new helpers in toolsRoute.ts. Updates environment and documentation to describe new LLM_ROUTER_TOOLS_MODEL config. Ensures fallback to Arch routing if no suitable model is found.
* Add <think> block handling for reasoning tokens
Introduces logic to merge provider-specific reasoning fields into <think> blocks within the token stream, mirroring OpenAI adapter behavior. Ensures <think> blocks are closed before final output and strips them from tool call messages to prevent confusion in follow-up reasoning.
* Update ChatInput.svelte
* Update ChatInput.svelte
* Update ChatInput.svelte
* Refactor formatting and fix indentation in server code
Improves code readability in toolsRoute.ts and runMcpFlow.ts by reformatting multi-line statements and correcting indentation. No functional changes were made.
* Handle user aborts quietly in runMcpFlow
* Remove namespaced tool aliases from OpenAI tools
* Update Switch.svelte
* Add navigation for tool update groups in chat
* Add MCP server favicon support and update config
* Fix router details rendering with missing metadata
Updates conditional rendering to check for the presence of 'route' in streamingRouterMetadata, preventing errors when metadata is undefined or missing the 'route' property.
* format
* Add model tool support indicator to chat UI
Introduces a `modelSupportsTools` prop to ChatInput and ChatWindow components to reflect whether the selected model supports tool calling. The MCP server indicator now visually changes and updates its tooltip based on tool support, improving user feedback for models without tool capabilities.
* Improve MCP health check URL validation and timeout
* Update server manager and card UI styles and labels
* Update prod.yaml
* Remove obsolete LLM log files
Deleted three outdated LLM log files related to chat completions for improved log management and reduced clutter.
* derive tool name in tool group
* Make MCP server count clickable to open manager
Replaces the MCP server count text in ChatInput with a button that opens the MCPServerManager. Also improves layout responsiveness in MCPServerManager for small screens.
* Update MCPServerManager.svelte
* Improve MCP server health check and tool response handling
* remove mcp oauth ...for now
* feat(chat): allow manual navigation of tool-call groups while streaming\n\n- Add toolAutoFollowLatest state to stop auto-snapping when user pages\n- Default to newest group while streaming; clamp index as groups change\n- Resume auto-follow when streaming ends or user returns to newest group\n\nAffects: src/lib/components/chat/ChatMessage.svelte
* feat(chat): show active tool call in footer and hide routing while calling\n\n- Derive current in-flight tool from assistant updates\n- Map to human-friendly displayName using page.data.tools\n- When a tool is actively running: render "Calling tool <name>" and skip the router/model status\n\nAffects: src/lib/components/chat/ChatWindow.svelte
* feat(mcp): optionally forward HF user token to official HF MCP endpoint\n\n- Add MCP_FORWARD_HF_USER_TOKEN config flag\n- Forward logged-in user's HF token to https://huggingface.co/mcp?login when no Authorization header is set\n- Apply overlay in runMcpFlow and in /api/mcp/health endpoint\n- Add shared helpers (hasAuthHeader, isStrictHfMcpLogin, hasNonEmptyToken)\n- Clarify client store comment about server-side overlay\n\nAffects: src/lib/server/config.ts, src/lib/server/textGeneration/mcp/runMcpFlow.ts, src/routes/api/mcp/health/+server.ts, src/lib/server/mcp/hf.ts, src/lib/stores/mcpServers.ts
* infra(chart): add read-mcp scope to OPENID_SCOPES for MCP access\n\n- Update dev and prod charts to request 'read-mcp' alongside 'openid profile inference-api'
* feat(prompt): include current date and image markdown hint in tool preprompt\n\n- Add today's date to tool preprompt for time-aware tools\n- Document how to inline generated images via markdown\n\nAffects: src/lib/server/textGeneration/utils/toolPrompt.ts
* chore(ui): minor formatting and cleanup in MCP ServerCard and conversation page import\n\n- Collapse named imports and remove stray blank lines in ServerCard\n- Normalize import whitespace in +page.svelte\n\nNo functional changes
* Update .env
* infra(chart): enable HF MCP t
- .env +12 -2
- .github/workflows/slugify.yaml +13 -13
- README.md +37 -0
- chart/env/dev.yaml +6 -1
- chart/env/prod.yaml +7 -2
- package-lock.json +934 -97
- package.json +1 -0
- src/hooks.server.ts +4 -0
- src/lib/components/NavMenu.svelte +23 -0
- src/lib/components/Switch.svelte +1 -1
- src/lib/components/chat/ChatInput.svelte +122 -2
- src/lib/components/chat/ChatMessage.svelte +60 -2
- src/lib/components/chat/ChatWindow.svelte +46 -2
- src/lib/components/chat/FileDropzone.svelte +1 -1
- src/lib/components/chat/MarkdownRenderer.svelte.test.ts +0 -54
- src/lib/components/chat/OpenReasoningResults.svelte +2 -2
- src/lib/components/chat/ToolUpdate.svelte +246 -0
- src/lib/components/icons/IconMCP.svelte +28 -0
- src/lib/components/mcp/AddServerForm.svelte +250 -0
- src/lib/components/mcp/MCPServerManager.svelte +185 -0
- src/lib/components/mcp/ServerCard.svelte +203 -0
- src/lib/server/api/routes/groups/models.ts +2 -0
- src/lib/server/api/routes/groups/user.ts +2 -2
- src/lib/server/config.ts +3 -1
- src/lib/server/endpoints/openai/endpointOai.ts +8 -13
- src/lib/server/endpoints/preprocessMessages.ts +19 -20
- src/lib/server/mcp/clientPool.ts +48 -0
- src/lib/server/mcp/hf.ts +21 -0
- src/lib/server/mcp/httpClient.ts +61 -0
- src/lib/server/mcp/registry.ts +76 -0
- src/lib/server/mcp/tools.ts +182 -0
- src/lib/server/models.ts +12 -0
- src/lib/server/router/endpoint.ts +49 -0
- src/lib/server/router/toolsRoute.ts +51 -0
- src/lib/server/textGeneration/generate.ts +177 -22
- src/lib/server/textGeneration/index.ts +30 -1
- src/lib/server/textGeneration/mcp/routerResolution.ts +105 -0
- src/lib/server/textGeneration/mcp/runMcpFlow.ts +554 -0
- src/lib/server/textGeneration/mcp/toolInvocation.ts +284 -0
- src/lib/server/textGeneration/reasoning.ts +23 -0
- src/lib/server/textGeneration/types.ts +2 -0
- src/lib/server/textGeneration/utils/routing.ts +21 -0
- src/lib/server/textGeneration/utils/toolPrompt.ts +15 -0
- src/lib/server/urlSafety.ts +25 -0
- src/lib/stores/mcpServers.ts +247 -0
- src/lib/stores/settings.ts +1 -0
- src/lib/types/Message.ts +2 -0
- src/lib/types/MessageUpdate.ts +40 -0
- src/lib/types/Settings.ts +7 -0
- src/lib/types/Tool.ts +74 -0
|
@@ -34,7 +34,7 @@ COUPLE_SESSION_WITH_COOKIE_NAME=
|
|
| 34 |
# when OPEN_ID is configured, users are required to login after the welcome modal
|
| 35 |
OPENID_CLIENT_ID=
|
| 36 |
OPENID_CLIENT_SECRET=
|
| 37 |
-
OPENID_SCOPES="openid profile inference-api"
|
| 38 |
USE_USER_TOKEN=
|
| 39 |
AUTOMATIC_LOGIN=# if true authentication is required on all routes
|
| 40 |
|
|
@@ -73,10 +73,15 @@ LLM_ROUTER_MAX_ASSISTANT_LENGTH=500
|
|
| 73 |
LLM_ROUTER_MAX_PREV_USER_LENGTH=400
|
| 74 |
|
| 75 |
# Enable router multimodal fallback (set to true to allow image inputs via router)
|
| 76 |
-
LLM_ROUTER_ENABLE_MULTIMODAL=
|
| 77 |
# Optional: specific model to use for multimodal requests. If not set, uses first multimodal model
|
| 78 |
LLM_ROUTER_MULTIMODAL_MODEL=
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
# Router UI overrides (client-visible)
|
| 81 |
# Public display name for the router entry in the model list. Defaults to "Omni".
|
| 82 |
PUBLIC_LLM_ROUTER_DISPLAY_NAME=Omni
|
|
@@ -113,6 +118,11 @@ ADMIN_TOKEN=#We recommend leaving this empty, you can get the token from the ter
|
|
| 113 |
LLM_SUMMARIZATION=true # generate conversation titles with LLMs
|
| 114 |
|
| 115 |
ALLOW_IFRAME=true # Allow the app to be embedded in an iframe
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
ENABLE_DATA_EXPORT=true
|
| 117 |
|
| 118 |
### Rate limits ###
|
|
|
|
| 34 |
# when OPEN_ID is configured, users are required to login after the welcome modal
|
| 35 |
OPENID_CLIENT_ID=
|
| 36 |
OPENID_CLIENT_SECRET=
|
| 37 |
+
OPENID_SCOPES="openid profile inference-api read-mcp"
|
| 38 |
USE_USER_TOKEN=
|
| 39 |
AUTOMATIC_LOGIN=# if true authentication is required on all routes
|
| 40 |
|
|
|
|
| 73 |
LLM_ROUTER_MAX_PREV_USER_LENGTH=400
|
| 74 |
|
| 75 |
# Enable router multimodal fallback (set to true to allow image inputs via router)
|
| 76 |
+
LLM_ROUTER_ENABLE_MULTIMODAL=
|
| 77 |
# Optional: specific model to use for multimodal requests. If not set, uses first multimodal model
|
| 78 |
LLM_ROUTER_MULTIMODAL_MODEL=
|
| 79 |
|
| 80 |
+
# Enable router tool support (set to true to allow tool calling via router)
|
| 81 |
+
LLM_ROUTER_ENABLE_TOOLS=
|
| 82 |
+
# Required when tools are active: id or name of the model to use for MCP tool calls.
|
| 83 |
+
LLM_ROUTER_TOOLS_MODEL=
|
| 84 |
+
|
| 85 |
# Router UI overrides (client-visible)
|
| 86 |
# Public display name for the router entry in the model list. Defaults to "Omni".
|
| 87 |
PUBLIC_LLM_ROUTER_DISPLAY_NAME=Omni
|
|
|
|
| 118 |
LLM_SUMMARIZATION=true # generate conversation titles with LLMs
|
| 119 |
|
| 120 |
ALLOW_IFRAME=true # Allow the app to be embedded in an iframe
|
| 121 |
+
|
| 122 |
+
# Base servers list (JSON array). Example: MCP_SERVERS=[{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"}, {"name": "Hugging Face", "url": "https://huggingface.co/mcp"}]
|
| 123 |
+
MCP_SERVERS=
|
| 124 |
+
# When true, forward the logged-in user's Hugging Face access token
|
| 125 |
+
MCP_FORWARD_HF_USER_TOKEN=
|
| 126 |
ENABLE_DATA_EXPORT=true
|
| 127 |
|
| 128 |
### Rate limits ###
|
|
@@ -4,12 +4,12 @@ on:
|
|
| 4 |
workflow_call:
|
| 5 |
inputs:
|
| 6 |
value:
|
| 7 |
-
description:
|
| 8 |
required: true
|
| 9 |
type: string
|
| 10 |
outputs:
|
| 11 |
slug:
|
| 12 |
-
description:
|
| 13 |
value: ${{ jobs.generate-slug.outputs.slug }}
|
| 14 |
|
| 15 |
jobs:
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
| 22 |
- name: Setup Go
|
| 23 |
uses: actions/setup-go@v5
|
| 24 |
with:
|
| 25 |
-
go-version:
|
| 26 |
|
| 27 |
- name: Generate slug
|
| 28 |
id: slugify
|
|
@@ -30,43 +30,43 @@ jobs:
|
|
| 30 |
# Create working directory
|
| 31 |
mkdir -p $HOME/slugify
|
| 32 |
cd $HOME/slugify
|
| 33 |
-
|
| 34 |
# Create Go script
|
| 35 |
cat > main.go << 'EOF'
|
| 36 |
package main
|
| 37 |
-
|
| 38 |
import (
|
| 39 |
"fmt"
|
| 40 |
"os"
|
| 41 |
"github.com/gosimple/slug"
|
| 42 |
)
|
| 43 |
-
|
| 44 |
func main() {
|
| 45 |
if len(os.Args) < 2 {
|
| 46 |
fmt.Println("Usage: slugify <text>")
|
| 47 |
os.Exit(1)
|
| 48 |
}
|
| 49 |
-
|
| 50 |
text := os.Args[1]
|
| 51 |
slugged := slug.Make(text)
|
| 52 |
fmt.Println(slugged)
|
| 53 |
}
|
| 54 |
EOF
|
| 55 |
-
|
| 56 |
# Initialize module and install dependency
|
| 57 |
go mod init slugify
|
| 58 |
go mod tidy
|
| 59 |
go get github.com/gosimple/slug
|
| 60 |
-
|
| 61 |
# Build
|
| 62 |
go build -o slugify main.go
|
| 63 |
-
|
| 64 |
# Generate slug
|
| 65 |
VALUE="${{ inputs.value }}"
|
| 66 |
echo "Input value: $VALUE"
|
| 67 |
-
|
| 68 |
SLUG=$(./slugify "$VALUE")
|
| 69 |
echo "Generated slug: $SLUG"
|
| 70 |
-
|
| 71 |
# Export
|
| 72 |
-
echo "slug=$SLUG" >> $GITHUB_OUTPUT
|
|
|
|
| 4 |
workflow_call:
|
| 5 |
inputs:
|
| 6 |
value:
|
| 7 |
+
description: "Value to slugify"
|
| 8 |
required: true
|
| 9 |
type: string
|
| 10 |
outputs:
|
| 11 |
slug:
|
| 12 |
+
description: "Slugified value"
|
| 13 |
value: ${{ jobs.generate-slug.outputs.slug }}
|
| 14 |
|
| 15 |
jobs:
|
|
|
|
| 22 |
- name: Setup Go
|
| 23 |
uses: actions/setup-go@v5
|
| 24 |
with:
|
| 25 |
+
go-version: "1.21"
|
| 26 |
|
| 27 |
- name: Generate slug
|
| 28 |
id: slugify
|
|
|
|
| 30 |
# Create working directory
|
| 31 |
mkdir -p $HOME/slugify
|
| 32 |
cd $HOME/slugify
|
| 33 |
+
|
| 34 |
# Create Go script
|
| 35 |
cat > main.go << 'EOF'
|
| 36 |
package main
|
| 37 |
+
|
| 38 |
import (
|
| 39 |
"fmt"
|
| 40 |
"os"
|
| 41 |
"github.com/gosimple/slug"
|
| 42 |
)
|
| 43 |
+
|
| 44 |
func main() {
|
| 45 |
if len(os.Args) < 2 {
|
| 46 |
fmt.Println("Usage: slugify <text>")
|
| 47 |
os.Exit(1)
|
| 48 |
}
|
| 49 |
+
|
| 50 |
text := os.Args[1]
|
| 51 |
slugged := slug.Make(text)
|
| 52 |
fmt.Println(slugged)
|
| 53 |
}
|
| 54 |
EOF
|
| 55 |
+
|
| 56 |
# Initialize module and install dependency
|
| 57 |
go mod init slugify
|
| 58 |
go mod tidy
|
| 59 |
go get github.com/gosimple/slug
|
| 60 |
+
|
| 61 |
# Build
|
| 62 |
go build -o slugify main.go
|
| 63 |
+
|
| 64 |
# Generate slug
|
| 65 |
VALUE="${{ inputs.value }}"
|
| 66 |
echo "Input value: $VALUE"
|
| 67 |
+
|
| 68 |
SLUG=$(./slugify "$VALUE")
|
| 69 |
echo "Generated slug: $SLUG"
|
| 70 |
+
|
| 71 |
# Export
|
| 72 |
+
echo "slug=$SLUG" >> $GITHUB_OUTPUT
|
|
@@ -142,6 +142,43 @@ When you select Omni in the UI, Chat UI will:
|
|
| 142 |
- Emit RouterMetadata immediately (route and actual model used) so the UI can display it.
|
| 143 |
- Stream from the selected model via your configured `OPENAI_BASE_URL`. On errors, it tries route fallbacks.
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
## Building
|
| 146 |
|
| 147 |
To create a production version of your app:
|
|
|
|
| 142 |
- Emit RouterMetadata immediately (route and actual model used) so the UI can display it.
|
| 143 |
- Stream from the selected model via your configured `OPENAI_BASE_URL`. On errors, it tries route fallbacks.
|
| 144 |
|
| 145 |
+
Tool and multimodal shortcuts:
|
| 146 |
+
|
| 147 |
+
- Multimodal: If `LLM_ROUTER_ENABLE_MULTIMODAL=true` and the user sends an image, the router bypasses Arch and uses `LLM_ROUTER_MULTIMODAL_MODEL` (or the first multimodal model). Route name: `multimodal`.
|
| 148 |
+
- Tools: If `LLM_ROUTER_ENABLE_TOOLS=true` and the user has at least one MCP server enabled, the router bypasses Arch and uses `LLM_ROUTER_TOOLS_MODEL`. If that model is missing or misconfigured, it falls back to Arch routing. Route name: `agentic`.
|
| 149 |
+
|
| 150 |
+
### MCP Tools (Optional)
|
| 151 |
+
|
| 152 |
+
Chat UI can call tools exposed by Model Context Protocol (MCP) servers and feed results back to the model using OpenAI function calling. You can preconfigure trusted servers via env, let users add their own, and optionally have the Omni router auto‑select a tools‑capable model.
|
| 153 |
+
|
| 154 |
+
Configure servers (base list for all users):
|
| 155 |
+
|
| 156 |
+
```env
|
| 157 |
+
# JSON array of servers: name, url, optional headers
|
| 158 |
+
MCP_SERVERS=[
|
| 159 |
+
{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"},
|
| 160 |
+
{"name": "Hugging Face MCP Login", "url": "https://huggingface.co/mcp?login"}
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
# Forward the signed-in user's Hugging Face token to the official HF MCP login endpoint
|
| 164 |
+
# when no Authorization header is set on that server entry.
|
| 165 |
+
MCP_FORWARD_HF_USER_TOKEN=true
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
Enable router tool path (Omni):
|
| 169 |
+
|
| 170 |
+
- Set `LLM_ROUTER_ENABLE_TOOLS=true` and choose a tools‑capable target with `LLM_ROUTER_TOOLS_MODEL=<model id or name>`.
|
| 171 |
+
- The target must support OpenAI tools/function calling. Chat UI surfaces a “tools” badge on models that advertise this; you can also force‑enable it per‑model in settings (see below).
|
| 172 |
+
|
| 173 |
+
Use tools in the UI:
|
| 174 |
+
|
| 175 |
+
- Open “MCP Servers” from the top‑right menu or from the `+` menu in the chat input to add servers, toggle them on, and run Health Check. The server card lists available tools.
|
| 176 |
+
- When a model calls a tool, the message shows a compact “tool” block with parameters, a progress bar while running, and the result (or error). Results are also provided back to the model for follow‑up.
|
| 177 |
+
|
| 178 |
+
Per‑model overrides:
|
| 179 |
+
|
| 180 |
+
- In Settings → Model, you can toggle “Tool calling (functions)” and “Multimodal input” per model. These overrides apply even if the provider metadata doesn’t advertise the capability.
|
| 181 |
+
|
| 182 |
## Building
|
| 183 |
|
| 184 |
To create a production version of your app:
|
|
@@ -38,8 +38,9 @@ ingressInternal:
|
|
| 38 |
envVars:
|
| 39 |
TEST: "test"
|
| 40 |
COUPLE_SESSION_WITH_COOKIE_NAME: "token"
|
| 41 |
-
OPENID_SCOPES: "openid profile inference-api"
|
| 42 |
USE_USER_TOKEN: "true"
|
|
|
|
| 43 |
AUTOMATIC_LOGIN: "false"
|
| 44 |
|
| 45 |
ADDRESS_HEADER: "X-Forwarded-For"
|
|
@@ -67,6 +68,10 @@ envVars:
|
|
| 67 |
LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
|
| 68 |
LLM_ROUTER_ENABLE_MULTIMODAL: "true"
|
| 69 |
LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
|
| 71 |
PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
|
| 72 |
PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
|
|
|
|
| 38 |
envVars:
|
| 39 |
TEST: "test"
|
| 40 |
COUPLE_SESSION_WITH_COOKIE_NAME: "token"
|
| 41 |
+
OPENID_SCOPES: "openid profile inference-api read-mcp"
|
| 42 |
USE_USER_TOKEN: "true"
|
| 43 |
+
MCP_FORWARD_HF_USER_TOKEN: "true"
|
| 44 |
AUTOMATIC_LOGIN: "false"
|
| 45 |
|
| 46 |
ADDRESS_HEADER: "X-Forwarded-For"
|
|
|
|
| 68 |
LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
|
| 69 |
LLM_ROUTER_ENABLE_MULTIMODAL: "true"
|
| 70 |
LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-235B-A22B-Thinking"
|
| 71 |
+
LLM_ROUTER_ENABLE_TOOLS: "true"
|
| 72 |
+
LLM_ROUTER_TOOLS_MODEL: "moonshotai/Kimi-K2-Instruct-0905"
|
| 73 |
+
MCP_SERVERS: >
|
| 74 |
+
[{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"}, {"name": "Hugging Face", "url": "https://huggingface.co/mcp?login"}]
|
| 75 |
PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
|
| 76 |
PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
|
| 77 |
PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
|
|
@@ -48,8 +48,9 @@ ingressInternal:
|
|
| 48 |
|
| 49 |
envVars:
|
| 50 |
COUPLE_SESSION_WITH_COOKIE_NAME: "token"
|
| 51 |
-
OPENID_SCOPES: "openid profile inference-api"
|
| 52 |
USE_USER_TOKEN: "true"
|
|
|
|
| 53 |
AUTOMATIC_LOGIN: "false"
|
| 54 |
|
| 55 |
ADDRESS_HEADER: "X-Forwarded-For"
|
|
@@ -76,7 +77,11 @@ envVars:
|
|
| 76 |
LLM_ROUTER_OTHER_ROUTE: "casual_conversation"
|
| 77 |
LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
|
| 78 |
LLM_ROUTER_ENABLE_MULTIMODAL: "true"
|
| 79 |
-
LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
|
| 81 |
PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
|
| 82 |
PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
|
|
|
|
| 48 |
|
| 49 |
envVars:
|
| 50 |
COUPLE_SESSION_WITH_COOKIE_NAME: "token"
|
| 51 |
+
OPENID_SCOPES: "openid profile inference-api read-mcp"
|
| 52 |
USE_USER_TOKEN: "true"
|
| 53 |
+
MCP_FORWARD_HF_USER_TOKEN: "true"
|
| 54 |
AUTOMATIC_LOGIN: "false"
|
| 55 |
|
| 56 |
ADDRESS_HEADER: "X-Forwarded-For"
|
|
|
|
| 77 |
LLM_ROUTER_OTHER_ROUTE: "casual_conversation"
|
| 78 |
LLM_ROUTER_ARCH_TIMEOUT_MS: "10000"
|
| 79 |
LLM_ROUTER_ENABLE_MULTIMODAL: "true"
|
| 80 |
+
LLM_ROUTER_MULTIMODAL_MODEL: "Qwen/Qwen3-VL-30B-A3B-Instruct"
|
| 81 |
+
LLM_ROUTER_ENABLE_TOOLS: "true"
|
| 82 |
+
LLM_ROUTER_TOOLS_MODEL: "moonshotai/Kimi-K2-Instruct-0905"
|
| 83 |
+
MCP_SERVERS: >
|
| 84 |
+
[{"name": "Web Search (Exa)", "url": "https://mcp.exa.ai/mcp"}, {"name": "Hugging Face", "url": "https://huggingface.co/mcp?login"}]
|
| 85 |
PUBLIC_LLM_ROUTER_DISPLAY_NAME: "Omni"
|
| 86 |
PUBLIC_LLM_ROUTER_LOGO_URL: "https://cdn-uploads.huggingface.co/production/uploads/5f17f0a0925b9863e28ad517/C5V0v1xZXv6M7FXsdJH9b.png"
|
| 87 |
PUBLIC_LLM_ROUTER_ALIAS_ID: "omni"
|
|
@@ -13,6 +13,7 @@
|
|
| 13 |
"@huggingface/hub": "^2.2.0",
|
| 14 |
"@huggingface/inference": "^4.11.3",
|
| 15 |
"@iconify-json/bi": "^1.1.21",
|
|
|
|
| 16 |
"@resvg/resvg-js": "^2.6.2",
|
| 17 |
"autoprefixer": "^10.4.14",
|
| 18 |
"aws4": "^1.13.2",
|
|
@@ -396,24 +397,24 @@
|
|
| 396 |
}
|
| 397 |
},
|
| 398 |
"node_modules/@aws-sdk/client-cognito-identity": {
|
| 399 |
-
"version": "3.
|
| 400 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.
|
| 401 |
-
"integrity": "sha512-
|
| 402 |
"license": "Apache-2.0",
|
| 403 |
"dependencies": {
|
| 404 |
"@aws-crypto/sha256-browser": "5.2.0",
|
| 405 |
"@aws-crypto/sha256-js": "5.2.0",
|
| 406 |
-
"@aws-sdk/core": "3.
|
| 407 |
-
"@aws-sdk/credential-provider-node": "3.
|
| 408 |
"@aws-sdk/middleware-host-header": "3.922.0",
|
| 409 |
"@aws-sdk/middleware-logger": "3.922.0",
|
| 410 |
"@aws-sdk/middleware-recursion-detection": "3.922.0",
|
| 411 |
-
"@aws-sdk/middleware-user-agent": "3.
|
| 412 |
"@aws-sdk/region-config-resolver": "3.925.0",
|
| 413 |
"@aws-sdk/types": "3.922.0",
|
| 414 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 415 |
"@aws-sdk/util-user-agent-browser": "3.922.0",
|
| 416 |
-
"@aws-sdk/util-user-agent-node": "3.
|
| 417 |
"@smithy/config-resolver": "^4.4.2",
|
| 418 |
"@smithy/core": "^3.17.2",
|
| 419 |
"@smithy/fetch-http-handler": "^5.3.5",
|
|
@@ -446,23 +447,23 @@
|
|
| 446 |
}
|
| 447 |
},
|
| 448 |
"node_modules/@aws-sdk/client-sso": {
|
| 449 |
-
"version": "3.
|
| 450 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.
|
| 451 |
-
"integrity": "sha512-
|
| 452 |
"license": "Apache-2.0",
|
| 453 |
"dependencies": {
|
| 454 |
"@aws-crypto/sha256-browser": "5.2.0",
|
| 455 |
"@aws-crypto/sha256-js": "5.2.0",
|
| 456 |
-
"@aws-sdk/core": "3.
|
| 457 |
"@aws-sdk/middleware-host-header": "3.922.0",
|
| 458 |
"@aws-sdk/middleware-logger": "3.922.0",
|
| 459 |
"@aws-sdk/middleware-recursion-detection": "3.922.0",
|
| 460 |
-
"@aws-sdk/middleware-user-agent": "3.
|
| 461 |
"@aws-sdk/region-config-resolver": "3.925.0",
|
| 462 |
"@aws-sdk/types": "3.922.0",
|
| 463 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 464 |
"@aws-sdk/util-user-agent-browser": "3.922.0",
|
| 465 |
-
"@aws-sdk/util-user-agent-node": "3.
|
| 466 |
"@smithy/config-resolver": "^4.4.2",
|
| 467 |
"@smithy/core": "^3.17.2",
|
| 468 |
"@smithy/fetch-http-handler": "^5.3.5",
|
|
@@ -495,9 +496,9 @@
|
|
| 495 |
}
|
| 496 |
},
|
| 497 |
"node_modules/@aws-sdk/core": {
|
| 498 |
-
"version": "3.
|
| 499 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.
|
| 500 |
-
"integrity": "sha512-
|
| 501 |
"license": "Apache-2.0",
|
| 502 |
"dependencies": {
|
| 503 |
"@aws-sdk/types": "3.922.0",
|
|
@@ -519,12 +520,12 @@
|
|
| 519 |
}
|
| 520 |
},
|
| 521 |
"node_modules/@aws-sdk/credential-provider-cognito-identity": {
|
| 522 |
-
"version": "3.
|
| 523 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.
|
| 524 |
-
"integrity": "sha512-
|
| 525 |
"license": "Apache-2.0",
|
| 526 |
"dependencies": {
|
| 527 |
-
"@aws-sdk/client-cognito-identity": "3.
|
| 528 |
"@aws-sdk/types": "3.922.0",
|
| 529 |
"@smithy/property-provider": "^4.2.4",
|
| 530 |
"@smithy/types": "^4.8.1",
|
|
@@ -535,12 +536,12 @@
|
|
| 535 |
}
|
| 536 |
},
|
| 537 |
"node_modules/@aws-sdk/credential-provider-env": {
|
| 538 |
-
"version": "3.
|
| 539 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.
|
| 540 |
-
"integrity": "sha512-
|
| 541 |
"license": "Apache-2.0",
|
| 542 |
"dependencies": {
|
| 543 |
-
"@aws-sdk/core": "3.
|
| 544 |
"@aws-sdk/types": "3.922.0",
|
| 545 |
"@smithy/property-provider": "^4.2.4",
|
| 546 |
"@smithy/types": "^4.8.1",
|
|
@@ -551,12 +552,12 @@
|
|
| 551 |
}
|
| 552 |
},
|
| 553 |
"node_modules/@aws-sdk/credential-provider-http": {
|
| 554 |
-
"version": "3.
|
| 555 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.
|
| 556 |
-
"integrity": "sha512-
|
| 557 |
"license": "Apache-2.0",
|
| 558 |
"dependencies": {
|
| 559 |
-
"@aws-sdk/core": "3.
|
| 560 |
"@aws-sdk/types": "3.922.0",
|
| 561 |
"@smithy/fetch-http-handler": "^5.3.5",
|
| 562 |
"@smithy/node-http-handler": "^4.4.4",
|
|
@@ -572,18 +573,18 @@
|
|
| 572 |
}
|
| 573 |
},
|
| 574 |
"node_modules/@aws-sdk/credential-provider-ini": {
|
| 575 |
-
"version": "3.
|
| 576 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.
|
| 577 |
-
"integrity": "sha512-
|
| 578 |
"license": "Apache-2.0",
|
| 579 |
"dependencies": {
|
| 580 |
-
"@aws-sdk/core": "3.
|
| 581 |
-
"@aws-sdk/credential-provider-env": "3.
|
| 582 |
-
"@aws-sdk/credential-provider-http": "3.
|
| 583 |
-
"@aws-sdk/credential-provider-process": "3.
|
| 584 |
-
"@aws-sdk/credential-provider-sso": "3.
|
| 585 |
-
"@aws-sdk/credential-provider-web-identity": "3.
|
| 586 |
-
"@aws-sdk/nested-clients": "3.
|
| 587 |
"@aws-sdk/types": "3.922.0",
|
| 588 |
"@smithy/credential-provider-imds": "^4.2.4",
|
| 589 |
"@smithy/property-provider": "^4.2.4",
|
|
@@ -596,17 +597,17 @@
|
|
| 596 |
}
|
| 597 |
},
|
| 598 |
"node_modules/@aws-sdk/credential-provider-node": {
|
| 599 |
-
"version": "3.
|
| 600 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.
|
| 601 |
-
"integrity": "sha512
|
| 602 |
"license": "Apache-2.0",
|
| 603 |
"dependencies": {
|
| 604 |
-
"@aws-sdk/credential-provider-env": "3.
|
| 605 |
-
"@aws-sdk/credential-provider-http": "3.
|
| 606 |
-
"@aws-sdk/credential-provider-ini": "3.
|
| 607 |
-
"@aws-sdk/credential-provider-process": "3.
|
| 608 |
-
"@aws-sdk/credential-provider-sso": "3.
|
| 609 |
-
"@aws-sdk/credential-provider-web-identity": "3.
|
| 610 |
"@aws-sdk/types": "3.922.0",
|
| 611 |
"@smithy/credential-provider-imds": "^4.2.4",
|
| 612 |
"@smithy/property-provider": "^4.2.4",
|
|
@@ -619,12 +620,12 @@
|
|
| 619 |
}
|
| 620 |
},
|
| 621 |
"node_modules/@aws-sdk/credential-provider-process": {
|
| 622 |
-
"version": "3.
|
| 623 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.
|
| 624 |
-
"integrity": "sha512-
|
| 625 |
"license": "Apache-2.0",
|
| 626 |
"dependencies": {
|
| 627 |
-
"@aws-sdk/core": "3.
|
| 628 |
"@aws-sdk/types": "3.922.0",
|
| 629 |
"@smithy/property-provider": "^4.2.4",
|
| 630 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
@@ -636,14 +637,14 @@
|
|
| 636 |
}
|
| 637 |
},
|
| 638 |
"node_modules/@aws-sdk/credential-provider-sso": {
|
| 639 |
-
"version": "3.
|
| 640 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.
|
| 641 |
-
"integrity": "sha512-
|
| 642 |
"license": "Apache-2.0",
|
| 643 |
"dependencies": {
|
| 644 |
-
"@aws-sdk/client-sso": "3.
|
| 645 |
-
"@aws-sdk/core": "3.
|
| 646 |
-
"@aws-sdk/token-providers": "3.
|
| 647 |
"@aws-sdk/types": "3.922.0",
|
| 648 |
"@smithy/property-provider": "^4.2.4",
|
| 649 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
@@ -655,13 +656,13 @@
|
|
| 655 |
}
|
| 656 |
},
|
| 657 |
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
| 658 |
-
"version": "3.
|
| 659 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.
|
| 660 |
-
"integrity": "sha512-
|
| 661 |
"license": "Apache-2.0",
|
| 662 |
"dependencies": {
|
| 663 |
-
"@aws-sdk/core": "3.
|
| 664 |
-
"@aws-sdk/nested-clients": "3.
|
| 665 |
"@aws-sdk/types": "3.922.0",
|
| 666 |
"@smithy/property-provider": "^4.2.4",
|
| 667 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
@@ -673,22 +674,22 @@
|
|
| 673 |
}
|
| 674 |
},
|
| 675 |
"node_modules/@aws-sdk/credential-providers": {
|
| 676 |
-
"version": "3.
|
| 677 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.
|
| 678 |
-
"integrity": "sha512-
|
| 679 |
"license": "Apache-2.0",
|
| 680 |
"dependencies": {
|
| 681 |
-
"@aws-sdk/client-cognito-identity": "3.
|
| 682 |
-
"@aws-sdk/core": "3.
|
| 683 |
-
"@aws-sdk/credential-provider-cognito-identity": "3.
|
| 684 |
-
"@aws-sdk/credential-provider-env": "3.
|
| 685 |
-
"@aws-sdk/credential-provider-http": "3.
|
| 686 |
-
"@aws-sdk/credential-provider-ini": "3.
|
| 687 |
-
"@aws-sdk/credential-provider-node": "3.
|
| 688 |
-
"@aws-sdk/credential-provider-process": "3.
|
| 689 |
-
"@aws-sdk/credential-provider-sso": "3.
|
| 690 |
-
"@aws-sdk/credential-provider-web-identity": "3.
|
| 691 |
-
"@aws-sdk/nested-clients": "3.
|
| 692 |
"@aws-sdk/types": "3.922.0",
|
| 693 |
"@smithy/config-resolver": "^4.4.2",
|
| 694 |
"@smithy/core": "^3.17.2",
|
|
@@ -748,12 +749,12 @@
|
|
| 748 |
}
|
| 749 |
},
|
| 750 |
"node_modules/@aws-sdk/middleware-user-agent": {
|
| 751 |
-
"version": "3.
|
| 752 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.
|
| 753 |
-
"integrity": "sha512-
|
| 754 |
"license": "Apache-2.0",
|
| 755 |
"dependencies": {
|
| 756 |
-
"@aws-sdk/core": "3.
|
| 757 |
"@aws-sdk/types": "3.922.0",
|
| 758 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 759 |
"@smithy/core": "^3.17.2",
|
|
@@ -766,23 +767,23 @@
|
|
| 766 |
}
|
| 767 |
},
|
| 768 |
"node_modules/@aws-sdk/nested-clients": {
|
| 769 |
-
"version": "3.
|
| 770 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.
|
| 771 |
-
"integrity": "sha512-
|
| 772 |
"license": "Apache-2.0",
|
| 773 |
"dependencies": {
|
| 774 |
"@aws-crypto/sha256-browser": "5.2.0",
|
| 775 |
"@aws-crypto/sha256-js": "5.2.0",
|
| 776 |
-
"@aws-sdk/core": "3.
|
| 777 |
"@aws-sdk/middleware-host-header": "3.922.0",
|
| 778 |
"@aws-sdk/middleware-logger": "3.922.0",
|
| 779 |
"@aws-sdk/middleware-recursion-detection": "3.922.0",
|
| 780 |
-
"@aws-sdk/middleware-user-agent": "3.
|
| 781 |
"@aws-sdk/region-config-resolver": "3.925.0",
|
| 782 |
"@aws-sdk/types": "3.922.0",
|
| 783 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 784 |
"@aws-sdk/util-user-agent-browser": "3.922.0",
|
| 785 |
-
"@aws-sdk/util-user-agent-node": "3.
|
| 786 |
"@smithy/config-resolver": "^4.4.2",
|
| 787 |
"@smithy/core": "^3.17.2",
|
| 788 |
"@smithy/fetch-http-handler": "^5.3.5",
|
|
@@ -831,13 +832,13 @@
|
|
| 831 |
}
|
| 832 |
},
|
| 833 |
"node_modules/@aws-sdk/token-providers": {
|
| 834 |
-
"version": "3.
|
| 835 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.
|
| 836 |
-
"integrity": "sha512-
|
| 837 |
"license": "Apache-2.0",
|
| 838 |
"dependencies": {
|
| 839 |
-
"@aws-sdk/core": "3.
|
| 840 |
-
"@aws-sdk/nested-clients": "3.
|
| 841 |
"@aws-sdk/types": "3.922.0",
|
| 842 |
"@smithy/property-provider": "^4.2.4",
|
| 843 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
@@ -902,12 +903,12 @@
|
|
| 902 |
}
|
| 903 |
},
|
| 904 |
"node_modules/@aws-sdk/util-user-agent-node": {
|
| 905 |
-
"version": "3.
|
| 906 |
-
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.
|
| 907 |
-
"integrity": "sha512-
|
| 908 |
"license": "Apache-2.0",
|
| 909 |
"dependencies": {
|
| 910 |
-
"@aws-sdk/middleware-user-agent": "3.
|
| 911 |
"@aws-sdk/types": "3.922.0",
|
| 912 |
"@smithy/node-config-provider": "^4.3.4",
|
| 913 |
"@smithy/types": "^4.8.1",
|
|
@@ -2421,6 +2422,60 @@
|
|
| 2421 |
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 2422 |
}
|
| 2423 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2424 |
"node_modules/@mongodb-js/saslprep": {
|
| 2425 |
"version": "1.2.2",
|
| 2426 |
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
|
|
@@ -4528,6 +4583,40 @@
|
|
| 4528 |
"node": ">=6.5"
|
| 4529 |
}
|
| 4530 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4531 |
"node_modules/acorn": {
|
| 4532 |
"version": "8.14.1",
|
| 4533 |
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
|
@@ -4589,6 +4678,45 @@
|
|
| 4589 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 4590 |
}
|
| 4591 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4592 |
"node_modules/ansi-escapes": {
|
| 4593 |
"version": "7.0.0",
|
| 4594 |
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
|
|
@@ -4868,6 +4996,26 @@
|
|
| 4868 |
"svelte": "^5.33.0"
|
| 4869 |
}
|
| 4870 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4871 |
"node_modules/bowser": {
|
| 4872 |
"version": "2.12.1",
|
| 4873 |
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz",
|
|
@@ -4986,6 +5134,15 @@
|
|
| 4986 |
"node": "*"
|
| 4987 |
}
|
| 4988 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4989 |
"node_modules/cac": {
|
| 4990 |
"version": "6.7.14",
|
| 4991 |
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
|
@@ -5008,6 +5165,22 @@
|
|
| 5008 |
"node": ">= 0.4"
|
| 5009 |
}
|
| 5010 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5011 |
"node_modules/callsites": {
|
| 5012 |
"version": "3.1.0",
|
| 5013 |
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
@@ -5255,6 +5428,27 @@
|
|
| 5255 |
"dev": true,
|
| 5256 |
"license": "MIT"
|
| 5257 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5258 |
"node_modules/cookie": {
|
| 5259 |
"version": "0.6.0",
|
| 5260 |
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
|
@@ -5265,6 +5459,15 @@
|
|
| 5265 |
"node": ">= 0.6"
|
| 5266 |
}
|
| 5267 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5268 |
"node_modules/copy-anything": {
|
| 5269 |
"version": "3.0.5",
|
| 5270 |
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
|
@@ -5281,6 +5484,19 @@
|
|
| 5281 |
"url": "https://github.com/sponsors/mesqueeb"
|
| 5282 |
}
|
| 5283 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5284 |
"node_modules/cross-spawn": {
|
| 5285 |
"version": "7.0.6",
|
| 5286 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
@@ -5448,6 +5664,15 @@
|
|
| 5448 |
"node": ">=0.4.0"
|
| 5449 |
}
|
| 5450 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5451 |
"node_modules/dequal": {
|
| 5452 |
"version": "2.0.3",
|
| 5453 |
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
|
@@ -5573,6 +5798,12 @@
|
|
| 5573 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
| 5574 |
"license": "MIT"
|
| 5575 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5576 |
"node_modules/electron-to-chromium": {
|
| 5577 |
"version": "1.5.165",
|
| 5578 |
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
|
|
@@ -5616,6 +5847,15 @@
|
|
| 5616 |
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
| 5617 |
"license": "MIT"
|
| 5618 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5619 |
"node_modules/end-of-stream": {
|
| 5620 |
"version": "1.4.4",
|
| 5621 |
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
|
@@ -6043,6 +6283,15 @@
|
|
| 6043 |
"node": ">=0.10.0"
|
| 6044 |
}
|
| 6045 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6046 |
"node_modules/event-target-shim": {
|
| 6047 |
"version": "5.0.1",
|
| 6048 |
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
|
@@ -6067,6 +6316,27 @@
|
|
| 6067 |
"node": ">=0.8.x"
|
| 6068 |
}
|
| 6069 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6070 |
"node_modules/exact-mirror": {
|
| 6071 |
"version": "0.1.2",
|
| 6072 |
"resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.1.2.tgz",
|
|
@@ -6125,6 +6395,93 @@
|
|
| 6125 |
"node": ">=12.0.0"
|
| 6126 |
}
|
| 6127 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6128 |
"node_modules/exsolve": {
|
| 6129 |
"version": "1.0.5",
|
| 6130 |
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
|
|
@@ -6148,7 +6505,6 @@
|
|
| 6148 |
"version": "3.1.3",
|
| 6149 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
| 6150 |
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
| 6151 |
-
"dev": true,
|
| 6152 |
"license": "MIT"
|
| 6153 |
},
|
| 6154 |
"node_modules/fast-fifo": {
|
|
@@ -6215,6 +6571,22 @@
|
|
| 6215 |
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
| 6216 |
"license": "MIT"
|
| 6217 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6218 |
"node_modules/fast-xml-parser": {
|
| 6219 |
"version": "5.2.5",
|
| 6220 |
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
|
@@ -6305,6 +6677,23 @@
|
|
| 6305 |
"node": ">=8"
|
| 6306 |
}
|
| 6307 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6308 |
"node_modules/find-cache-dir": {
|
| 6309 |
"version": "3.3.2",
|
| 6310 |
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
|
|
@@ -6434,6 +6823,15 @@
|
|
| 6434 |
"node": ">= 12.20"
|
| 6435 |
}
|
| 6436 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6437 |
"node_modules/fraction.js": {
|
| 6438 |
"version": "4.3.7",
|
| 6439 |
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
|
@@ -6447,6 +6845,15 @@
|
|
| 6447 |
"url": "https://github.com/sponsors/rawify"
|
| 6448 |
}
|
| 6449 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6450 |
"node_modules/fs.realpath": {
|
| 6451 |
"version": "1.0.0",
|
| 6452 |
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
@@ -6766,6 +7173,31 @@
|
|
| 6766 |
"node": ">=12"
|
| 6767 |
}
|
| 6768 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6769 |
"node_modules/http-proxy-agent": {
|
| 6770 |
"version": "5.0.0",
|
| 6771 |
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
|
@@ -6924,7 +7356,6 @@
|
|
| 6924 |
"version": "2.0.4",
|
| 6925 |
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 6926 |
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 6927 |
-
"dev": true,
|
| 6928 |
"license": "ISC"
|
| 6929 |
},
|
| 6930 |
"node_modules/inline-style-parser": {
|
|
@@ -6952,6 +7383,15 @@
|
|
| 6952 |
"node": ">= 12"
|
| 6953 |
}
|
| 6954 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6955 |
"node_modules/is-arrayish": {
|
| 6956 |
"version": "0.3.2",
|
| 6957 |
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
|
@@ -7050,6 +7490,12 @@
|
|
| 7050 |
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
| 7051 |
"license": "MIT"
|
| 7052 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7053 |
"node_modules/is-reference": {
|
| 7054 |
"version": "1.2.1",
|
| 7055 |
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
|
@@ -7854,6 +8300,15 @@
|
|
| 7854 |
"node": ">= 0.4"
|
| 7855 |
}
|
| 7856 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7857 |
"node_modules/memory-pager": {
|
| 7858 |
"version": "1.5.0",
|
| 7859 |
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
|
@@ -7861,6 +8316,18 @@
|
|
| 7861 |
"devOptional": true,
|
| 7862 |
"license": "MIT"
|
| 7863 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7864 |
"node_modules/merge-stream": {
|
| 7865 |
"version": "2.0.0",
|
| 7866 |
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
|
@@ -8281,6 +8748,15 @@
|
|
| 8281 |
"dev": true,
|
| 8282 |
"license": "MIT"
|
| 8283 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8284 |
"node_modules/neo-async": {
|
| 8285 |
"version": "2.6.2",
|
| 8286 |
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
|
@@ -8443,6 +8919,18 @@
|
|
| 8443 |
"node": ">= 6"
|
| 8444 |
}
|
| 8445 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8446 |
"node_modules/object-stream": {
|
| 8447 |
"version": "0.0.1",
|
| 8448 |
"resolved": "https://registry.npmjs.org/object-stream/-/object-stream-0.0.1.tgz",
|
|
@@ -8469,6 +8957,18 @@
|
|
| 8469 |
"node": ">=14.0.0"
|
| 8470 |
}
|
| 8471 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8472 |
"node_modules/once": {
|
| 8473 |
"version": "1.4.0",
|
| 8474 |
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
@@ -8712,6 +9212,15 @@
|
|
| 8712 |
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 8713 |
}
|
| 8714 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8715 |
"node_modules/path-exists": {
|
| 8716 |
"version": "4.0.0",
|
| 8717 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
@@ -8769,6 +9278,16 @@
|
|
| 8769 |
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
| 8770 |
"license": "ISC"
|
| 8771 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8772 |
"node_modules/path-type": {
|
| 8773 |
"version": "4.0.0",
|
| 8774 |
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
|
@@ -8924,6 +9443,15 @@
|
|
| 8924 |
"node": ">= 6"
|
| 8925 |
}
|
| 8926 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8927 |
"node_modules/pkg-dir": {
|
| 8928 |
"version": "4.2.0",
|
| 8929 |
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
|
@@ -9460,6 +9988,19 @@
|
|
| 9460 |
"node": "^16 || ^18 || >=20"
|
| 9461 |
}
|
| 9462 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9463 |
"node_modules/psl": {
|
| 9464 |
"version": "1.15.0",
|
| 9465 |
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
|
@@ -9502,6 +10043,21 @@
|
|
| 9502 |
"teleport": ">=0.2.0"
|
| 9503 |
}
|
| 9504 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9505 |
"node_modules/quansync": {
|
| 9506 |
"version": "0.2.10",
|
| 9507 |
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
|
@@ -9551,6 +10107,46 @@
|
|
| 9551 |
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
| 9552 |
"license": "MIT"
|
| 9553 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9554 |
"node_modules/react-is": {
|
| 9555 |
"version": "17.0.2",
|
| 9556 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
|
@@ -9606,6 +10202,15 @@
|
|
| 9606 |
"node": ">= 12.13.0"
|
| 9607 |
}
|
| 9608 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9609 |
"node_modules/requires-port": {
|
| 9610 |
"version": "1.0.0",
|
| 9611 |
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
|
@@ -9751,6 +10356,22 @@
|
|
| 9751 |
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
| 9752 |
"license": "MIT"
|
| 9753 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9754 |
"node_modules/rrweb-cssom": {
|
| 9755 |
"version": "0.6.0",
|
| 9756 |
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
|
|
@@ -9912,6 +10533,64 @@
|
|
| 9912 |
"node": ">=10"
|
| 9913 |
}
|
| 9914 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9915 |
"node_modules/set-cookie-parser": {
|
| 9916 |
"version": "2.7.1",
|
| 9917 |
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
|
@@ -9919,6 +10598,12 @@
|
|
| 9919 |
"devOptional": true,
|
| 9920 |
"license": "MIT"
|
| 9921 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9922 |
"node_modules/sharp": {
|
| 9923 |
"version": "0.33.5",
|
| 9924 |
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
|
@@ -9979,6 +10664,78 @@
|
|
| 9979 |
"node": ">=8"
|
| 9980 |
}
|
| 9981 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9982 |
"node_modules/siginfo": {
|
| 9983 |
"version": "2.0.0",
|
| 9984 |
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
|
@@ -10146,6 +10903,15 @@
|
|
| 10146 |
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
| 10147 |
"license": "MIT"
|
| 10148 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10149 |
"node_modules/std-env": {
|
| 10150 |
"version": "3.9.0",
|
| 10151 |
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
|
|
@@ -10911,6 +11677,15 @@
|
|
| 10911 |
"node": ">=8.0"
|
| 10912 |
}
|
| 10913 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10914 |
"node_modules/token-types": {
|
| 10915 |
"version": "6.0.0",
|
| 10916 |
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz",
|
|
@@ -11015,6 +11790,41 @@
|
|
| 11015 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 11016 |
}
|
| 11017 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11018 |
"node_modules/typescript": {
|
| 11019 |
"version": "5.8.3",
|
| 11020 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
|
@@ -11100,6 +11910,15 @@
|
|
| 11100 |
"node": ">= 4.0.0"
|
| 11101 |
}
|
| 11102 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11103 |
"node_modules/unplugin": {
|
| 11104 |
"version": "1.16.1",
|
| 11105 |
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
|
|
@@ -11232,6 +12051,15 @@
|
|
| 11232 |
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==",
|
| 11233 |
"license": "MIT"
|
| 11234 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11235 |
"node_modules/vite": {
|
| 11236 |
"version": "6.3.5",
|
| 11237 |
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
|
@@ -11819,6 +12647,15 @@
|
|
| 11819 |
"funding": {
|
| 11820 |
"url": "https://github.com/sponsors/colinhacks"
|
| 11821 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11822 |
}
|
| 11823 |
}
|
| 11824 |
}
|
|
|
|
| 13 |
"@huggingface/hub": "^2.2.0",
|
| 14 |
"@huggingface/inference": "^4.11.3",
|
| 15 |
"@iconify-json/bi": "^1.1.21",
|
| 16 |
+
"@modelcontextprotocol/sdk": "^1.21.1",
|
| 17 |
"@resvg/resvg-js": "^2.6.2",
|
| 18 |
"autoprefixer": "^10.4.14",
|
| 19 |
"aws4": "^1.13.2",
|
|
|
|
| 397 |
}
|
| 398 |
},
|
| 399 |
"node_modules/@aws-sdk/client-cognito-identity": {
|
| 400 |
+
"version": "3.927.0",
|
| 401 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.927.0.tgz",
|
| 402 |
+
"integrity": "sha512-nt6qcS94C88jV3ZVzc7nG4ew4Wrbi27UsYFB8OpvLNFSXOTWx3Sd7g7xn6FyRFBM6QH+zijqgQ6lpKIMQdm9+w==",
|
| 403 |
"license": "Apache-2.0",
|
| 404 |
"dependencies": {
|
| 405 |
"@aws-crypto/sha256-browser": "5.2.0",
|
| 406 |
"@aws-crypto/sha256-js": "5.2.0",
|
| 407 |
+
"@aws-sdk/core": "3.927.0",
|
| 408 |
+
"@aws-sdk/credential-provider-node": "3.927.0",
|
| 409 |
"@aws-sdk/middleware-host-header": "3.922.0",
|
| 410 |
"@aws-sdk/middleware-logger": "3.922.0",
|
| 411 |
"@aws-sdk/middleware-recursion-detection": "3.922.0",
|
| 412 |
+
"@aws-sdk/middleware-user-agent": "3.927.0",
|
| 413 |
"@aws-sdk/region-config-resolver": "3.925.0",
|
| 414 |
"@aws-sdk/types": "3.922.0",
|
| 415 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 416 |
"@aws-sdk/util-user-agent-browser": "3.922.0",
|
| 417 |
+
"@aws-sdk/util-user-agent-node": "3.927.0",
|
| 418 |
"@smithy/config-resolver": "^4.4.2",
|
| 419 |
"@smithy/core": "^3.17.2",
|
| 420 |
"@smithy/fetch-http-handler": "^5.3.5",
|
|
|
|
| 447 |
}
|
| 448 |
},
|
| 449 |
"node_modules/@aws-sdk/client-sso": {
|
| 450 |
+
"version": "3.927.0",
|
| 451 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.927.0.tgz",
|
| 452 |
+
"integrity": "sha512-O+e+jo6ei7U/BA7lhT4mmPCWmeR9dFgGUHVwCwJ5c/nCaSaHQ+cb7j2h8WPXERu0LhPSFyj1aD5dk3jFIwNlbg==",
|
| 453 |
"license": "Apache-2.0",
|
| 454 |
"dependencies": {
|
| 455 |
"@aws-crypto/sha256-browser": "5.2.0",
|
| 456 |
"@aws-crypto/sha256-js": "5.2.0",
|
| 457 |
+
"@aws-sdk/core": "3.927.0",
|
| 458 |
"@aws-sdk/middleware-host-header": "3.922.0",
|
| 459 |
"@aws-sdk/middleware-logger": "3.922.0",
|
| 460 |
"@aws-sdk/middleware-recursion-detection": "3.922.0",
|
| 461 |
+
"@aws-sdk/middleware-user-agent": "3.927.0",
|
| 462 |
"@aws-sdk/region-config-resolver": "3.925.0",
|
| 463 |
"@aws-sdk/types": "3.922.0",
|
| 464 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 465 |
"@aws-sdk/util-user-agent-browser": "3.922.0",
|
| 466 |
+
"@aws-sdk/util-user-agent-node": "3.927.0",
|
| 467 |
"@smithy/config-resolver": "^4.4.2",
|
| 468 |
"@smithy/core": "^3.17.2",
|
| 469 |
"@smithy/fetch-http-handler": "^5.3.5",
|
|
|
|
| 496 |
}
|
| 497 |
},
|
| 498 |
"node_modules/@aws-sdk/core": {
|
| 499 |
+
"version": "3.927.0",
|
| 500 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.927.0.tgz",
|
| 501 |
+
"integrity": "sha512-QOtR9QdjNeC7bId3fc/6MnqoEezvQ2Fk+x6F+Auf7NhOxwYAtB1nvh0k3+gJHWVGpfxN1I8keahRZd79U68/ag==",
|
| 502 |
"license": "Apache-2.0",
|
| 503 |
"dependencies": {
|
| 504 |
"@aws-sdk/types": "3.922.0",
|
|
|
|
| 520 |
}
|
| 521 |
},
|
| 522 |
"node_modules/@aws-sdk/credential-provider-cognito-identity": {
|
| 523 |
+
"version": "3.927.0",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.927.0.tgz",
|
| 525 |
+
"integrity": "sha512-zV6w71IT+7rTUiIBIdzHt0aDkYA0NckZHr97/O6qcp0qm3mIj8oiDjHo6sD8qLAVT2ixmAhuBuZ8DAkMHjZ0wA==",
|
| 526 |
"license": "Apache-2.0",
|
| 527 |
"dependencies": {
|
| 528 |
+
"@aws-sdk/client-cognito-identity": "3.927.0",
|
| 529 |
"@aws-sdk/types": "3.922.0",
|
| 530 |
"@smithy/property-provider": "^4.2.4",
|
| 531 |
"@smithy/types": "^4.8.1",
|
|
|
|
| 536 |
}
|
| 537 |
},
|
| 538 |
"node_modules/@aws-sdk/credential-provider-env": {
|
| 539 |
+
"version": "3.927.0",
|
| 540 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.927.0.tgz",
|
| 541 |
+
"integrity": "sha512-bAllBpmaWINpf0brXQWh/hjkBctapknZPYb3FJRlBHytEGHi7TpgqBXi8riT0tc6RVWChhnw58rQz22acOmBuw==",
|
| 542 |
"license": "Apache-2.0",
|
| 543 |
"dependencies": {
|
| 544 |
+
"@aws-sdk/core": "3.927.0",
|
| 545 |
"@aws-sdk/types": "3.922.0",
|
| 546 |
"@smithy/property-provider": "^4.2.4",
|
| 547 |
"@smithy/types": "^4.8.1",
|
|
|
|
| 552 |
}
|
| 553 |
},
|
| 554 |
"node_modules/@aws-sdk/credential-provider-http": {
|
| 555 |
+
"version": "3.927.0",
|
| 556 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.927.0.tgz",
|
| 557 |
+
"integrity": "sha512-jEvb8C7tuRBFhe8vZY9vm9z6UQnbP85IMEt3Qiz0dxAd341Hgu0lOzMv5mSKQ5yBnTLq+t3FPKgD9tIiHLqxSQ==",
|
| 558 |
"license": "Apache-2.0",
|
| 559 |
"dependencies": {
|
| 560 |
+
"@aws-sdk/core": "3.927.0",
|
| 561 |
"@aws-sdk/types": "3.922.0",
|
| 562 |
"@smithy/fetch-http-handler": "^5.3.5",
|
| 563 |
"@smithy/node-http-handler": "^4.4.4",
|
|
|
|
| 573 |
}
|
| 574 |
},
|
| 575 |
"node_modules/@aws-sdk/credential-provider-ini": {
|
| 576 |
+
"version": "3.927.0",
|
| 577 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.927.0.tgz",
|
| 578 |
+
"integrity": "sha512-WvliaKYT7bNLiryl/FsZyUwRGBo/CWtboekZWvSfloAb+0SKFXWjmxt3z+Y260aoaPm/LIzEyslDHfxqR9xCJQ==",
|
| 579 |
"license": "Apache-2.0",
|
| 580 |
"dependencies": {
|
| 581 |
+
"@aws-sdk/core": "3.927.0",
|
| 582 |
+
"@aws-sdk/credential-provider-env": "3.927.0",
|
| 583 |
+
"@aws-sdk/credential-provider-http": "3.927.0",
|
| 584 |
+
"@aws-sdk/credential-provider-process": "3.927.0",
|
| 585 |
+
"@aws-sdk/credential-provider-sso": "3.927.0",
|
| 586 |
+
"@aws-sdk/credential-provider-web-identity": "3.927.0",
|
| 587 |
+
"@aws-sdk/nested-clients": "3.927.0",
|
| 588 |
"@aws-sdk/types": "3.922.0",
|
| 589 |
"@smithy/credential-provider-imds": "^4.2.4",
|
| 590 |
"@smithy/property-provider": "^4.2.4",
|
|
|
|
| 597 |
}
|
| 598 |
},
|
| 599 |
"node_modules/@aws-sdk/credential-provider-node": {
|
| 600 |
+
"version": "3.927.0",
|
| 601 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.927.0.tgz",
|
| 602 |
+
"integrity": "sha512-M6BLrI+WHQ7PUY1aYu2OkI/KEz9aca+05zyycACk7cnlHlZaQ3vTFd0xOqF+A1qaenQBuxApOTs7Z21pnPUo9Q==",
|
| 603 |
"license": "Apache-2.0",
|
| 604 |
"dependencies": {
|
| 605 |
+
"@aws-sdk/credential-provider-env": "3.927.0",
|
| 606 |
+
"@aws-sdk/credential-provider-http": "3.927.0",
|
| 607 |
+
"@aws-sdk/credential-provider-ini": "3.927.0",
|
| 608 |
+
"@aws-sdk/credential-provider-process": "3.927.0",
|
| 609 |
+
"@aws-sdk/credential-provider-sso": "3.927.0",
|
| 610 |
+
"@aws-sdk/credential-provider-web-identity": "3.927.0",
|
| 611 |
"@aws-sdk/types": "3.922.0",
|
| 612 |
"@smithy/credential-provider-imds": "^4.2.4",
|
| 613 |
"@smithy/property-provider": "^4.2.4",
|
|
|
|
| 620 |
}
|
| 621 |
},
|
| 622 |
"node_modules/@aws-sdk/credential-provider-process": {
|
| 623 |
+
"version": "3.927.0",
|
| 624 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.927.0.tgz",
|
| 625 |
+
"integrity": "sha512-rvqdZIN3TRhLKssufN5G2EWLMBct3ZebOBdwr0tuOoPEdaYflyXYYUScu+Beb541CKfXaFnEOlZokq12r7EPcQ==",
|
| 626 |
"license": "Apache-2.0",
|
| 627 |
"dependencies": {
|
| 628 |
+
"@aws-sdk/core": "3.927.0",
|
| 629 |
"@aws-sdk/types": "3.922.0",
|
| 630 |
"@smithy/property-provider": "^4.2.4",
|
| 631 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
|
|
| 637 |
}
|
| 638 |
},
|
| 639 |
"node_modules/@aws-sdk/credential-provider-sso": {
|
| 640 |
+
"version": "3.927.0",
|
| 641 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.927.0.tgz",
|
| 642 |
+
"integrity": "sha512-XrCuncze/kxZE6WYEWtNMGtrJvJtyhUqav4xQQ9PJcNjxCUYiIRv7Gwkt7cuwJ1HS+akQj+JiZmljAg97utfDw==",
|
| 643 |
"license": "Apache-2.0",
|
| 644 |
"dependencies": {
|
| 645 |
+
"@aws-sdk/client-sso": "3.927.0",
|
| 646 |
+
"@aws-sdk/core": "3.927.0",
|
| 647 |
+
"@aws-sdk/token-providers": "3.927.0",
|
| 648 |
"@aws-sdk/types": "3.922.0",
|
| 649 |
"@smithy/property-provider": "^4.2.4",
|
| 650 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
|
|
| 656 |
}
|
| 657 |
},
|
| 658 |
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
| 659 |
+
"version": "3.927.0",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.927.0.tgz",
|
| 661 |
+
"integrity": "sha512-Oh/aFYjZQsIiZ2PQEgTNvqEE/mmOYxZKZzXV86qrU3jBUfUUBvprUZc684nBqJbSKPwM5jCZtxiRYh+IrZDE7A==",
|
| 662 |
"license": "Apache-2.0",
|
| 663 |
"dependencies": {
|
| 664 |
+
"@aws-sdk/core": "3.927.0",
|
| 665 |
+
"@aws-sdk/nested-clients": "3.927.0",
|
| 666 |
"@aws-sdk/types": "3.922.0",
|
| 667 |
"@smithy/property-provider": "^4.2.4",
|
| 668 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
|
|
| 674 |
}
|
| 675 |
},
|
| 676 |
"node_modules/@aws-sdk/credential-providers": {
|
| 677 |
+
"version": "3.927.0",
|
| 678 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.927.0.tgz",
|
| 679 |
+
"integrity": "sha512-CasoHKKE/K+6YcVqjE+v5dVyKqKBtfzZyvGi669HvJ1f4EPHbVRPPLIb0eAYd/aEmwHsB/nn9VnyN9Wq5OppUQ==",
|
| 680 |
"license": "Apache-2.0",
|
| 681 |
"dependencies": {
|
| 682 |
+
"@aws-sdk/client-cognito-identity": "3.927.0",
|
| 683 |
+
"@aws-sdk/core": "3.927.0",
|
| 684 |
+
"@aws-sdk/credential-provider-cognito-identity": "3.927.0",
|
| 685 |
+
"@aws-sdk/credential-provider-env": "3.927.0",
|
| 686 |
+
"@aws-sdk/credential-provider-http": "3.927.0",
|
| 687 |
+
"@aws-sdk/credential-provider-ini": "3.927.0",
|
| 688 |
+
"@aws-sdk/credential-provider-node": "3.927.0",
|
| 689 |
+
"@aws-sdk/credential-provider-process": "3.927.0",
|
| 690 |
+
"@aws-sdk/credential-provider-sso": "3.927.0",
|
| 691 |
+
"@aws-sdk/credential-provider-web-identity": "3.927.0",
|
| 692 |
+
"@aws-sdk/nested-clients": "3.927.0",
|
| 693 |
"@aws-sdk/types": "3.922.0",
|
| 694 |
"@smithy/config-resolver": "^4.4.2",
|
| 695 |
"@smithy/core": "^3.17.2",
|
|
|
|
| 749 |
}
|
| 750 |
},
|
| 751 |
"node_modules/@aws-sdk/middleware-user-agent": {
|
| 752 |
+
"version": "3.927.0",
|
| 753 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.927.0.tgz",
|
| 754 |
+
"integrity": "sha512-sv6St9EgEka6E7y19UMCsttFBZ8tsmz2sstgRd7LztlX3wJynpeDUhq0gtedguG1lGZY/gDf832k5dqlRLUk7g==",
|
| 755 |
"license": "Apache-2.0",
|
| 756 |
"dependencies": {
|
| 757 |
+
"@aws-sdk/core": "3.927.0",
|
| 758 |
"@aws-sdk/types": "3.922.0",
|
| 759 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 760 |
"@smithy/core": "^3.17.2",
|
|
|
|
| 767 |
}
|
| 768 |
},
|
| 769 |
"node_modules/@aws-sdk/nested-clients": {
|
| 770 |
+
"version": "3.927.0",
|
| 771 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.927.0.tgz",
|
| 772 |
+
"integrity": "sha512-Oy6w7+fzIdr10DhF/HpfVLy6raZFTdiE7pxS1rvpuj2JgxzW2y6urm2sYf3eLOpMiHyuG4xUBwFiJpU9CCEvJA==",
|
| 773 |
"license": "Apache-2.0",
|
| 774 |
"dependencies": {
|
| 775 |
"@aws-crypto/sha256-browser": "5.2.0",
|
| 776 |
"@aws-crypto/sha256-js": "5.2.0",
|
| 777 |
+
"@aws-sdk/core": "3.927.0",
|
| 778 |
"@aws-sdk/middleware-host-header": "3.922.0",
|
| 779 |
"@aws-sdk/middleware-logger": "3.922.0",
|
| 780 |
"@aws-sdk/middleware-recursion-detection": "3.922.0",
|
| 781 |
+
"@aws-sdk/middleware-user-agent": "3.927.0",
|
| 782 |
"@aws-sdk/region-config-resolver": "3.925.0",
|
| 783 |
"@aws-sdk/types": "3.922.0",
|
| 784 |
"@aws-sdk/util-endpoints": "3.922.0",
|
| 785 |
"@aws-sdk/util-user-agent-browser": "3.922.0",
|
| 786 |
+
"@aws-sdk/util-user-agent-node": "3.927.0",
|
| 787 |
"@smithy/config-resolver": "^4.4.2",
|
| 788 |
"@smithy/core": "^3.17.2",
|
| 789 |
"@smithy/fetch-http-handler": "^5.3.5",
|
|
|
|
| 832 |
}
|
| 833 |
},
|
| 834 |
"node_modules/@aws-sdk/token-providers": {
|
| 835 |
+
"version": "3.927.0",
|
| 836 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.927.0.tgz",
|
| 837 |
+
"integrity": "sha512-JRdaprkZjZ6EY4WVwsZaEjPUj9W9vqlSaFDm4oD+IbwlY4GjAXuUQK6skKcvVyoOsSTvJp/CaveSws2FiWUp9Q==",
|
| 838 |
"license": "Apache-2.0",
|
| 839 |
"dependencies": {
|
| 840 |
+
"@aws-sdk/core": "3.927.0",
|
| 841 |
+
"@aws-sdk/nested-clients": "3.927.0",
|
| 842 |
"@aws-sdk/types": "3.922.0",
|
| 843 |
"@smithy/property-provider": "^4.2.4",
|
| 844 |
"@smithy/shared-ini-file-loader": "^4.3.4",
|
|
|
|
| 903 |
}
|
| 904 |
},
|
| 905 |
"node_modules/@aws-sdk/util-user-agent-node": {
|
| 906 |
+
"version": "3.927.0",
|
| 907 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.927.0.tgz",
|
| 908 |
+
"integrity": "sha512-5Ty+29jBTHg1mathEhLJavzA7A7vmhephRYGenFzo8rApLZh+c+MCAqjddSjdDzcf5FH+ydGGnIrj4iIfbZIMQ==",
|
| 909 |
"license": "Apache-2.0",
|
| 910 |
"dependencies": {
|
| 911 |
+
"@aws-sdk/middleware-user-agent": "3.927.0",
|
| 912 |
"@aws-sdk/types": "3.922.0",
|
| 913 |
"@smithy/node-config-provider": "^4.3.4",
|
| 914 |
"@smithy/types": "^4.8.1",
|
|
|
|
| 2422 |
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 2423 |
}
|
| 2424 |
},
|
| 2425 |
+
"node_modules/@modelcontextprotocol/sdk": {
|
| 2426 |
+
"version": "1.21.1",
|
| 2427 |
+
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.1.tgz",
|
| 2428 |
+
"integrity": "sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==",
|
| 2429 |
+
"license": "MIT",
|
| 2430 |
+
"dependencies": {
|
| 2431 |
+
"ajv": "^8.17.1",
|
| 2432 |
+
"ajv-formats": "^3.0.1",
|
| 2433 |
+
"content-type": "^1.0.5",
|
| 2434 |
+
"cors": "^2.8.5",
|
| 2435 |
+
"cross-spawn": "^7.0.5",
|
| 2436 |
+
"eventsource": "^3.0.2",
|
| 2437 |
+
"eventsource-parser": "^3.0.0",
|
| 2438 |
+
"express": "^5.0.1",
|
| 2439 |
+
"express-rate-limit": "^7.5.0",
|
| 2440 |
+
"pkce-challenge": "^5.0.0",
|
| 2441 |
+
"raw-body": "^3.0.0",
|
| 2442 |
+
"zod": "^3.23.8",
|
| 2443 |
+
"zod-to-json-schema": "^3.24.1"
|
| 2444 |
+
},
|
| 2445 |
+
"engines": {
|
| 2446 |
+
"node": ">=18"
|
| 2447 |
+
},
|
| 2448 |
+
"peerDependencies": {
|
| 2449 |
+
"@cfworker/json-schema": "^4.1.1"
|
| 2450 |
+
},
|
| 2451 |
+
"peerDependenciesMeta": {
|
| 2452 |
+
"@cfworker/json-schema": {
|
| 2453 |
+
"optional": true
|
| 2454 |
+
}
|
| 2455 |
+
}
|
| 2456 |
+
},
|
| 2457 |
+
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
|
| 2458 |
+
"version": "8.17.1",
|
| 2459 |
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
| 2460 |
+
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
| 2461 |
+
"license": "MIT",
|
| 2462 |
+
"dependencies": {
|
| 2463 |
+
"fast-deep-equal": "^3.1.3",
|
| 2464 |
+
"fast-uri": "^3.0.1",
|
| 2465 |
+
"json-schema-traverse": "^1.0.0",
|
| 2466 |
+
"require-from-string": "^2.0.2"
|
| 2467 |
+
},
|
| 2468 |
+
"funding": {
|
| 2469 |
+
"type": "github",
|
| 2470 |
+
"url": "https://github.com/sponsors/epoberezkin"
|
| 2471 |
+
}
|
| 2472 |
+
},
|
| 2473 |
+
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
|
| 2474 |
+
"version": "1.0.0",
|
| 2475 |
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
| 2476 |
+
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
| 2477 |
+
"license": "MIT"
|
| 2478 |
+
},
|
| 2479 |
"node_modules/@mongodb-js/saslprep": {
|
| 2480 |
"version": "1.2.2",
|
| 2481 |
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.2.tgz",
|
|
|
|
| 4583 |
"node": ">=6.5"
|
| 4584 |
}
|
| 4585 |
},
|
| 4586 |
+
"node_modules/accepts": {
|
| 4587 |
+
"version": "2.0.0",
|
| 4588 |
+
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
| 4589 |
+
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
| 4590 |
+
"license": "MIT",
|
| 4591 |
+
"dependencies": {
|
| 4592 |
+
"mime-types": "^3.0.0",
|
| 4593 |
+
"negotiator": "^1.0.0"
|
| 4594 |
+
},
|
| 4595 |
+
"engines": {
|
| 4596 |
+
"node": ">= 0.6"
|
| 4597 |
+
}
|
| 4598 |
+
},
|
| 4599 |
+
"node_modules/accepts/node_modules/mime-db": {
|
| 4600 |
+
"version": "1.54.0",
|
| 4601 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
| 4602 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
| 4603 |
+
"license": "MIT",
|
| 4604 |
+
"engines": {
|
| 4605 |
+
"node": ">= 0.6"
|
| 4606 |
+
}
|
| 4607 |
+
},
|
| 4608 |
+
"node_modules/accepts/node_modules/mime-types": {
|
| 4609 |
+
"version": "3.0.1",
|
| 4610 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
| 4611 |
+
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
| 4612 |
+
"license": "MIT",
|
| 4613 |
+
"dependencies": {
|
| 4614 |
+
"mime-db": "^1.54.0"
|
| 4615 |
+
},
|
| 4616 |
+
"engines": {
|
| 4617 |
+
"node": ">= 0.6"
|
| 4618 |
+
}
|
| 4619 |
+
},
|
| 4620 |
"node_modules/acorn": {
|
| 4621 |
"version": "8.14.1",
|
| 4622 |
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
|
|
|
| 4678 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 4679 |
}
|
| 4680 |
},
|
| 4681 |
+
"node_modules/ajv-formats": {
|
| 4682 |
+
"version": "3.0.1",
|
| 4683 |
+
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
| 4684 |
+
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
| 4685 |
+
"license": "MIT",
|
| 4686 |
+
"dependencies": {
|
| 4687 |
+
"ajv": "^8.0.0"
|
| 4688 |
+
},
|
| 4689 |
+
"peerDependencies": {
|
| 4690 |
+
"ajv": "^8.0.0"
|
| 4691 |
+
},
|
| 4692 |
+
"peerDependenciesMeta": {
|
| 4693 |
+
"ajv": {
|
| 4694 |
+
"optional": true
|
| 4695 |
+
}
|
| 4696 |
+
}
|
| 4697 |
+
},
|
| 4698 |
+
"node_modules/ajv-formats/node_modules/ajv": {
|
| 4699 |
+
"version": "8.17.1",
|
| 4700 |
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
| 4701 |
+
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
| 4702 |
+
"license": "MIT",
|
| 4703 |
+
"dependencies": {
|
| 4704 |
+
"fast-deep-equal": "^3.1.3",
|
| 4705 |
+
"fast-uri": "^3.0.1",
|
| 4706 |
+
"json-schema-traverse": "^1.0.0",
|
| 4707 |
+
"require-from-string": "^2.0.2"
|
| 4708 |
+
},
|
| 4709 |
+
"funding": {
|
| 4710 |
+
"type": "github",
|
| 4711 |
+
"url": "https://github.com/sponsors/epoberezkin"
|
| 4712 |
+
}
|
| 4713 |
+
},
|
| 4714 |
+
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
| 4715 |
+
"version": "1.0.0",
|
| 4716 |
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
| 4717 |
+
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
| 4718 |
+
"license": "MIT"
|
| 4719 |
+
},
|
| 4720 |
"node_modules/ansi-escapes": {
|
| 4721 |
"version": "7.0.0",
|
| 4722 |
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
|
|
|
|
| 4996 |
"svelte": "^5.33.0"
|
| 4997 |
}
|
| 4998 |
},
|
| 4999 |
+
"node_modules/body-parser": {
|
| 5000 |
+
"version": "2.2.0",
|
| 5001 |
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
| 5002 |
+
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
| 5003 |
+
"license": "MIT",
|
| 5004 |
+
"dependencies": {
|
| 5005 |
+
"bytes": "^3.1.2",
|
| 5006 |
+
"content-type": "^1.0.5",
|
| 5007 |
+
"debug": "^4.4.0",
|
| 5008 |
+
"http-errors": "^2.0.0",
|
| 5009 |
+
"iconv-lite": "^0.6.3",
|
| 5010 |
+
"on-finished": "^2.4.1",
|
| 5011 |
+
"qs": "^6.14.0",
|
| 5012 |
+
"raw-body": "^3.0.0",
|
| 5013 |
+
"type-is": "^2.0.0"
|
| 5014 |
+
},
|
| 5015 |
+
"engines": {
|
| 5016 |
+
"node": ">=18"
|
| 5017 |
+
}
|
| 5018 |
+
},
|
| 5019 |
"node_modules/bowser": {
|
| 5020 |
"version": "2.12.1",
|
| 5021 |
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz",
|
|
|
|
| 5134 |
"node": "*"
|
| 5135 |
}
|
| 5136 |
},
|
| 5137 |
+
"node_modules/bytes": {
|
| 5138 |
+
"version": "3.1.2",
|
| 5139 |
+
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
| 5140 |
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
| 5141 |
+
"license": "MIT",
|
| 5142 |
+
"engines": {
|
| 5143 |
+
"node": ">= 0.8"
|
| 5144 |
+
}
|
| 5145 |
+
},
|
| 5146 |
"node_modules/cac": {
|
| 5147 |
"version": "6.7.14",
|
| 5148 |
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
|
|
|
| 5165 |
"node": ">= 0.4"
|
| 5166 |
}
|
| 5167 |
},
|
| 5168 |
+
"node_modules/call-bound": {
|
| 5169 |
+
"version": "1.0.4",
|
| 5170 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
| 5171 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
| 5172 |
+
"license": "MIT",
|
| 5173 |
+
"dependencies": {
|
| 5174 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 5175 |
+
"get-intrinsic": "^1.3.0"
|
| 5176 |
+
},
|
| 5177 |
+
"engines": {
|
| 5178 |
+
"node": ">= 0.4"
|
| 5179 |
+
},
|
| 5180 |
+
"funding": {
|
| 5181 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 5182 |
+
}
|
| 5183 |
+
},
|
| 5184 |
"node_modules/callsites": {
|
| 5185 |
"version": "3.1.0",
|
| 5186 |
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
|
|
|
| 5428 |
"dev": true,
|
| 5429 |
"license": "MIT"
|
| 5430 |
},
|
| 5431 |
+
"node_modules/content-disposition": {
|
| 5432 |
+
"version": "1.0.0",
|
| 5433 |
+
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
| 5434 |
+
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
|
| 5435 |
+
"license": "MIT",
|
| 5436 |
+
"dependencies": {
|
| 5437 |
+
"safe-buffer": "5.2.1"
|
| 5438 |
+
},
|
| 5439 |
+
"engines": {
|
| 5440 |
+
"node": ">= 0.6"
|
| 5441 |
+
}
|
| 5442 |
+
},
|
| 5443 |
+
"node_modules/content-type": {
|
| 5444 |
+
"version": "1.0.5",
|
| 5445 |
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
| 5446 |
+
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
| 5447 |
+
"license": "MIT",
|
| 5448 |
+
"engines": {
|
| 5449 |
+
"node": ">= 0.6"
|
| 5450 |
+
}
|
| 5451 |
+
},
|
| 5452 |
"node_modules/cookie": {
|
| 5453 |
"version": "0.6.0",
|
| 5454 |
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
|
|
|
| 5459 |
"node": ">= 0.6"
|
| 5460 |
}
|
| 5461 |
},
|
| 5462 |
+
"node_modules/cookie-signature": {
|
| 5463 |
+
"version": "1.2.2",
|
| 5464 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
| 5465 |
+
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
| 5466 |
+
"license": "MIT",
|
| 5467 |
+
"engines": {
|
| 5468 |
+
"node": ">=6.6.0"
|
| 5469 |
+
}
|
| 5470 |
+
},
|
| 5471 |
"node_modules/copy-anything": {
|
| 5472 |
"version": "3.0.5",
|
| 5473 |
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
|
|
|
| 5484 |
"url": "https://github.com/sponsors/mesqueeb"
|
| 5485 |
}
|
| 5486 |
},
|
| 5487 |
+
"node_modules/cors": {
|
| 5488 |
+
"version": "2.8.5",
|
| 5489 |
+
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
| 5490 |
+
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
| 5491 |
+
"license": "MIT",
|
| 5492 |
+
"dependencies": {
|
| 5493 |
+
"object-assign": "^4",
|
| 5494 |
+
"vary": "^1"
|
| 5495 |
+
},
|
| 5496 |
+
"engines": {
|
| 5497 |
+
"node": ">= 0.10"
|
| 5498 |
+
}
|
| 5499 |
+
},
|
| 5500 |
"node_modules/cross-spawn": {
|
| 5501 |
"version": "7.0.6",
|
| 5502 |
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
|
|
| 5664 |
"node": ">=0.4.0"
|
| 5665 |
}
|
| 5666 |
},
|
| 5667 |
+
"node_modules/depd": {
|
| 5668 |
+
"version": "2.0.0",
|
| 5669 |
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
| 5670 |
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
| 5671 |
+
"license": "MIT",
|
| 5672 |
+
"engines": {
|
| 5673 |
+
"node": ">= 0.8"
|
| 5674 |
+
}
|
| 5675 |
+
},
|
| 5676 |
"node_modules/dequal": {
|
| 5677 |
"version": "2.0.3",
|
| 5678 |
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
|
|
|
| 5798 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
| 5799 |
"license": "MIT"
|
| 5800 |
},
|
| 5801 |
+
"node_modules/ee-first": {
|
| 5802 |
+
"version": "1.1.1",
|
| 5803 |
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
| 5804 |
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
| 5805 |
+
"license": "MIT"
|
| 5806 |
+
},
|
| 5807 |
"node_modules/electron-to-chromium": {
|
| 5808 |
"version": "1.5.165",
|
| 5809 |
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
|
|
|
|
| 5847 |
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
| 5848 |
"license": "MIT"
|
| 5849 |
},
|
| 5850 |
+
"node_modules/encodeurl": {
|
| 5851 |
+
"version": "2.0.0",
|
| 5852 |
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
| 5853 |
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
| 5854 |
+
"license": "MIT",
|
| 5855 |
+
"engines": {
|
| 5856 |
+
"node": ">= 0.8"
|
| 5857 |
+
}
|
| 5858 |
+
},
|
| 5859 |
"node_modules/end-of-stream": {
|
| 5860 |
"version": "1.4.4",
|
| 5861 |
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
|
|
|
| 6283 |
"node": ">=0.10.0"
|
| 6284 |
}
|
| 6285 |
},
|
| 6286 |
+
"node_modules/etag": {
|
| 6287 |
+
"version": "1.8.1",
|
| 6288 |
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
| 6289 |
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
| 6290 |
+
"license": "MIT",
|
| 6291 |
+
"engines": {
|
| 6292 |
+
"node": ">= 0.6"
|
| 6293 |
+
}
|
| 6294 |
+
},
|
| 6295 |
"node_modules/event-target-shim": {
|
| 6296 |
"version": "5.0.1",
|
| 6297 |
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
|
|
|
| 6316 |
"node": ">=0.8.x"
|
| 6317 |
}
|
| 6318 |
},
|
| 6319 |
+
"node_modules/eventsource": {
|
| 6320 |
+
"version": "3.0.7",
|
| 6321 |
+
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
| 6322 |
+
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
| 6323 |
+
"license": "MIT",
|
| 6324 |
+
"dependencies": {
|
| 6325 |
+
"eventsource-parser": "^3.0.1"
|
| 6326 |
+
},
|
| 6327 |
+
"engines": {
|
| 6328 |
+
"node": ">=18.0.0"
|
| 6329 |
+
}
|
| 6330 |
+
},
|
| 6331 |
+
"node_modules/eventsource-parser": {
|
| 6332 |
+
"version": "3.0.6",
|
| 6333 |
+
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
|
| 6334 |
+
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
|
| 6335 |
+
"license": "MIT",
|
| 6336 |
+
"engines": {
|
| 6337 |
+
"node": ">=18.0.0"
|
| 6338 |
+
}
|
| 6339 |
+
},
|
| 6340 |
"node_modules/exact-mirror": {
|
| 6341 |
"version": "0.1.2",
|
| 6342 |
"resolved": "https://registry.npmjs.org/exact-mirror/-/exact-mirror-0.1.2.tgz",
|
|
|
|
| 6395 |
"node": ">=12.0.0"
|
| 6396 |
}
|
| 6397 |
},
|
| 6398 |
+
"node_modules/express": {
|
| 6399 |
+
"version": "5.1.0",
|
| 6400 |
+
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
| 6401 |
+
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
| 6402 |
+
"license": "MIT",
|
| 6403 |
+
"dependencies": {
|
| 6404 |
+
"accepts": "^2.0.0",
|
| 6405 |
+
"body-parser": "^2.2.0",
|
| 6406 |
+
"content-disposition": "^1.0.0",
|
| 6407 |
+
"content-type": "^1.0.5",
|
| 6408 |
+
"cookie": "^0.7.1",
|
| 6409 |
+
"cookie-signature": "^1.2.1",
|
| 6410 |
+
"debug": "^4.4.0",
|
| 6411 |
+
"encodeurl": "^2.0.0",
|
| 6412 |
+
"escape-html": "^1.0.3",
|
| 6413 |
+
"etag": "^1.8.1",
|
| 6414 |
+
"finalhandler": "^2.1.0",
|
| 6415 |
+
"fresh": "^2.0.0",
|
| 6416 |
+
"http-errors": "^2.0.0",
|
| 6417 |
+
"merge-descriptors": "^2.0.0",
|
| 6418 |
+
"mime-types": "^3.0.0",
|
| 6419 |
+
"on-finished": "^2.4.1",
|
| 6420 |
+
"once": "^1.4.0",
|
| 6421 |
+
"parseurl": "^1.3.3",
|
| 6422 |
+
"proxy-addr": "^2.0.7",
|
| 6423 |
+
"qs": "^6.14.0",
|
| 6424 |
+
"range-parser": "^1.2.1",
|
| 6425 |
+
"router": "^2.2.0",
|
| 6426 |
+
"send": "^1.1.0",
|
| 6427 |
+
"serve-static": "^2.2.0",
|
| 6428 |
+
"statuses": "^2.0.1",
|
| 6429 |
+
"type-is": "^2.0.1",
|
| 6430 |
+
"vary": "^1.1.2"
|
| 6431 |
+
},
|
| 6432 |
+
"engines": {
|
| 6433 |
+
"node": ">= 18"
|
| 6434 |
+
},
|
| 6435 |
+
"funding": {
|
| 6436 |
+
"type": "opencollective",
|
| 6437 |
+
"url": "https://opencollective.com/express"
|
| 6438 |
+
}
|
| 6439 |
+
},
|
| 6440 |
+
"node_modules/express-rate-limit": {
|
| 6441 |
+
"version": "7.5.1",
|
| 6442 |
+
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
| 6443 |
+
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
| 6444 |
+
"license": "MIT",
|
| 6445 |
+
"engines": {
|
| 6446 |
+
"node": ">= 16"
|
| 6447 |
+
},
|
| 6448 |
+
"funding": {
|
| 6449 |
+
"url": "https://github.com/sponsors/express-rate-limit"
|
| 6450 |
+
},
|
| 6451 |
+
"peerDependencies": {
|
| 6452 |
+
"express": ">= 4.11"
|
| 6453 |
+
}
|
| 6454 |
+
},
|
| 6455 |
+
"node_modules/express/node_modules/cookie": {
|
| 6456 |
+
"version": "0.7.2",
|
| 6457 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
| 6458 |
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
| 6459 |
+
"license": "MIT",
|
| 6460 |
+
"engines": {
|
| 6461 |
+
"node": ">= 0.6"
|
| 6462 |
+
}
|
| 6463 |
+
},
|
| 6464 |
+
"node_modules/express/node_modules/mime-db": {
|
| 6465 |
+
"version": "1.54.0",
|
| 6466 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
| 6467 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
| 6468 |
+
"license": "MIT",
|
| 6469 |
+
"engines": {
|
| 6470 |
+
"node": ">= 0.6"
|
| 6471 |
+
}
|
| 6472 |
+
},
|
| 6473 |
+
"node_modules/express/node_modules/mime-types": {
|
| 6474 |
+
"version": "3.0.1",
|
| 6475 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
| 6476 |
+
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
| 6477 |
+
"license": "MIT",
|
| 6478 |
+
"dependencies": {
|
| 6479 |
+
"mime-db": "^1.54.0"
|
| 6480 |
+
},
|
| 6481 |
+
"engines": {
|
| 6482 |
+
"node": ">= 0.6"
|
| 6483 |
+
}
|
| 6484 |
+
},
|
| 6485 |
"node_modules/exsolve": {
|
| 6486 |
"version": "1.0.5",
|
| 6487 |
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
|
|
|
|
| 6505 |
"version": "3.1.3",
|
| 6506 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
| 6507 |
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
|
|
|
| 6508 |
"license": "MIT"
|
| 6509 |
},
|
| 6510 |
"node_modules/fast-fifo": {
|
|
|
|
| 6571 |
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
| 6572 |
"license": "MIT"
|
| 6573 |
},
|
| 6574 |
+
"node_modules/fast-uri": {
|
| 6575 |
+
"version": "3.1.0",
|
| 6576 |
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
| 6577 |
+
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
| 6578 |
+
"funding": [
|
| 6579 |
+
{
|
| 6580 |
+
"type": "github",
|
| 6581 |
+
"url": "https://github.com/sponsors/fastify"
|
| 6582 |
+
},
|
| 6583 |
+
{
|
| 6584 |
+
"type": "opencollective",
|
| 6585 |
+
"url": "https://opencollective.com/fastify"
|
| 6586 |
+
}
|
| 6587 |
+
],
|
| 6588 |
+
"license": "BSD-3-Clause"
|
| 6589 |
+
},
|
| 6590 |
"node_modules/fast-xml-parser": {
|
| 6591 |
"version": "5.2.5",
|
| 6592 |
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
|
|
|
| 6677 |
"node": ">=8"
|
| 6678 |
}
|
| 6679 |
},
|
| 6680 |
+
"node_modules/finalhandler": {
|
| 6681 |
+
"version": "2.1.0",
|
| 6682 |
+
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
| 6683 |
+
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
| 6684 |
+
"license": "MIT",
|
| 6685 |
+
"dependencies": {
|
| 6686 |
+
"debug": "^4.4.0",
|
| 6687 |
+
"encodeurl": "^2.0.0",
|
| 6688 |
+
"escape-html": "^1.0.3",
|
| 6689 |
+
"on-finished": "^2.4.1",
|
| 6690 |
+
"parseurl": "^1.3.3",
|
| 6691 |
+
"statuses": "^2.0.1"
|
| 6692 |
+
},
|
| 6693 |
+
"engines": {
|
| 6694 |
+
"node": ">= 0.8"
|
| 6695 |
+
}
|
| 6696 |
+
},
|
| 6697 |
"node_modules/find-cache-dir": {
|
| 6698 |
"version": "3.3.2",
|
| 6699 |
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
|
|
|
|
| 6823 |
"node": ">= 12.20"
|
| 6824 |
}
|
| 6825 |
},
|
| 6826 |
+
"node_modules/forwarded": {
|
| 6827 |
+
"version": "0.2.0",
|
| 6828 |
+
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
| 6829 |
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
| 6830 |
+
"license": "MIT",
|
| 6831 |
+
"engines": {
|
| 6832 |
+
"node": ">= 0.6"
|
| 6833 |
+
}
|
| 6834 |
+
},
|
| 6835 |
"node_modules/fraction.js": {
|
| 6836 |
"version": "4.3.7",
|
| 6837 |
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
|
|
|
| 6845 |
"url": "https://github.com/sponsors/rawify"
|
| 6846 |
}
|
| 6847 |
},
|
| 6848 |
+
"node_modules/fresh": {
|
| 6849 |
+
"version": "2.0.0",
|
| 6850 |
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
| 6851 |
+
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
| 6852 |
+
"license": "MIT",
|
| 6853 |
+
"engines": {
|
| 6854 |
+
"node": ">= 0.8"
|
| 6855 |
+
}
|
| 6856 |
+
},
|
| 6857 |
"node_modules/fs.realpath": {
|
| 6858 |
"version": "1.0.0",
|
| 6859 |
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
|
|
| 7173 |
"node": ">=12"
|
| 7174 |
}
|
| 7175 |
},
|
| 7176 |
+
"node_modules/http-errors": {
|
| 7177 |
+
"version": "2.0.0",
|
| 7178 |
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
| 7179 |
+
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
| 7180 |
+
"license": "MIT",
|
| 7181 |
+
"dependencies": {
|
| 7182 |
+
"depd": "2.0.0",
|
| 7183 |
+
"inherits": "2.0.4",
|
| 7184 |
+
"setprototypeof": "1.2.0",
|
| 7185 |
+
"statuses": "2.0.1",
|
| 7186 |
+
"toidentifier": "1.0.1"
|
| 7187 |
+
},
|
| 7188 |
+
"engines": {
|
| 7189 |
+
"node": ">= 0.8"
|
| 7190 |
+
}
|
| 7191 |
+
},
|
| 7192 |
+
"node_modules/http-errors/node_modules/statuses": {
|
| 7193 |
+
"version": "2.0.1",
|
| 7194 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
| 7195 |
+
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
| 7196 |
+
"license": "MIT",
|
| 7197 |
+
"engines": {
|
| 7198 |
+
"node": ">= 0.8"
|
| 7199 |
+
}
|
| 7200 |
+
},
|
| 7201 |
"node_modules/http-proxy-agent": {
|
| 7202 |
"version": "5.0.0",
|
| 7203 |
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
|
|
|
| 7356 |
"version": "2.0.4",
|
| 7357 |
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 7358 |
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
|
|
|
| 7359 |
"license": "ISC"
|
| 7360 |
},
|
| 7361 |
"node_modules/inline-style-parser": {
|
|
|
|
| 7383 |
"node": ">= 12"
|
| 7384 |
}
|
| 7385 |
},
|
| 7386 |
+
"node_modules/ipaddr.js": {
|
| 7387 |
+
"version": "1.9.1",
|
| 7388 |
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
| 7389 |
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
| 7390 |
+
"license": "MIT",
|
| 7391 |
+
"engines": {
|
| 7392 |
+
"node": ">= 0.10"
|
| 7393 |
+
}
|
| 7394 |
+
},
|
| 7395 |
"node_modules/is-arrayish": {
|
| 7396 |
"version": "0.3.2",
|
| 7397 |
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
|
|
|
| 7490 |
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
| 7491 |
"license": "MIT"
|
| 7492 |
},
|
| 7493 |
+
"node_modules/is-promise": {
|
| 7494 |
+
"version": "4.0.0",
|
| 7495 |
+
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
| 7496 |
+
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
| 7497 |
+
"license": "MIT"
|
| 7498 |
+
},
|
| 7499 |
"node_modules/is-reference": {
|
| 7500 |
"version": "1.2.1",
|
| 7501 |
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
|
|
|
| 8300 |
"node": ">= 0.4"
|
| 8301 |
}
|
| 8302 |
},
|
| 8303 |
+
"node_modules/media-typer": {
|
| 8304 |
+
"version": "1.1.0",
|
| 8305 |
+
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
| 8306 |
+
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
| 8307 |
+
"license": "MIT",
|
| 8308 |
+
"engines": {
|
| 8309 |
+
"node": ">= 0.8"
|
| 8310 |
+
}
|
| 8311 |
+
},
|
| 8312 |
"node_modules/memory-pager": {
|
| 8313 |
"version": "1.5.0",
|
| 8314 |
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
|
|
|
| 8316 |
"devOptional": true,
|
| 8317 |
"license": "MIT"
|
| 8318 |
},
|
| 8319 |
+
"node_modules/merge-descriptors": {
|
| 8320 |
+
"version": "2.0.0",
|
| 8321 |
+
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
| 8322 |
+
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
| 8323 |
+
"license": "MIT",
|
| 8324 |
+
"engines": {
|
| 8325 |
+
"node": ">=18"
|
| 8326 |
+
},
|
| 8327 |
+
"funding": {
|
| 8328 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 8329 |
+
}
|
| 8330 |
+
},
|
| 8331 |
"node_modules/merge-stream": {
|
| 8332 |
"version": "2.0.0",
|
| 8333 |
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
|
|
|
| 8748 |
"dev": true,
|
| 8749 |
"license": "MIT"
|
| 8750 |
},
|
| 8751 |
+
"node_modules/negotiator": {
|
| 8752 |
+
"version": "1.0.0",
|
| 8753 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
| 8754 |
+
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
| 8755 |
+
"license": "MIT",
|
| 8756 |
+
"engines": {
|
| 8757 |
+
"node": ">= 0.6"
|
| 8758 |
+
}
|
| 8759 |
+
},
|
| 8760 |
"node_modules/neo-async": {
|
| 8761 |
"version": "2.6.2",
|
| 8762 |
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
|
|
|
| 8919 |
"node": ">= 6"
|
| 8920 |
}
|
| 8921 |
},
|
| 8922 |
+
"node_modules/object-inspect": {
|
| 8923 |
+
"version": "1.13.4",
|
| 8924 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
| 8925 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
| 8926 |
+
"license": "MIT",
|
| 8927 |
+
"engines": {
|
| 8928 |
+
"node": ">= 0.4"
|
| 8929 |
+
},
|
| 8930 |
+
"funding": {
|
| 8931 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 8932 |
+
}
|
| 8933 |
+
},
|
| 8934 |
"node_modules/object-stream": {
|
| 8935 |
"version": "0.0.1",
|
| 8936 |
"resolved": "https://registry.npmjs.org/object-stream/-/object-stream-0.0.1.tgz",
|
|
|
|
| 8957 |
"node": ">=14.0.0"
|
| 8958 |
}
|
| 8959 |
},
|
| 8960 |
+
"node_modules/on-finished": {
|
| 8961 |
+
"version": "2.4.1",
|
| 8962 |
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
| 8963 |
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
| 8964 |
+
"license": "MIT",
|
| 8965 |
+
"dependencies": {
|
| 8966 |
+
"ee-first": "1.1.1"
|
| 8967 |
+
},
|
| 8968 |
+
"engines": {
|
| 8969 |
+
"node": ">= 0.8"
|
| 8970 |
+
}
|
| 8971 |
+
},
|
| 8972 |
"node_modules/once": {
|
| 8973 |
"version": "1.4.0",
|
| 8974 |
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
|
|
| 9212 |
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 9213 |
}
|
| 9214 |
},
|
| 9215 |
+
"node_modules/parseurl": {
|
| 9216 |
+
"version": "1.3.3",
|
| 9217 |
+
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
| 9218 |
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
| 9219 |
+
"license": "MIT",
|
| 9220 |
+
"engines": {
|
| 9221 |
+
"node": ">= 0.8"
|
| 9222 |
+
}
|
| 9223 |
+
},
|
| 9224 |
"node_modules/path-exists": {
|
| 9225 |
"version": "4.0.0",
|
| 9226 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
|
|
| 9278 |
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
| 9279 |
"license": "ISC"
|
| 9280 |
},
|
| 9281 |
+
"node_modules/path-to-regexp": {
|
| 9282 |
+
"version": "8.3.0",
|
| 9283 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
| 9284 |
+
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
| 9285 |
+
"license": "MIT",
|
| 9286 |
+
"funding": {
|
| 9287 |
+
"type": "opencollective",
|
| 9288 |
+
"url": "https://opencollective.com/express"
|
| 9289 |
+
}
|
| 9290 |
+
},
|
| 9291 |
"node_modules/path-type": {
|
| 9292 |
"version": "4.0.0",
|
| 9293 |
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
|
|
|
| 9443 |
"node": ">= 6"
|
| 9444 |
}
|
| 9445 |
},
|
| 9446 |
+
"node_modules/pkce-challenge": {
|
| 9447 |
+
"version": "5.0.0",
|
| 9448 |
+
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
|
| 9449 |
+
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
|
| 9450 |
+
"license": "MIT",
|
| 9451 |
+
"engines": {
|
| 9452 |
+
"node": ">=16.20.0"
|
| 9453 |
+
}
|
| 9454 |
+
},
|
| 9455 |
"node_modules/pkg-dir": {
|
| 9456 |
"version": "4.2.0",
|
| 9457 |
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
|
|
|
| 9988 |
"node": "^16 || ^18 || >=20"
|
| 9989 |
}
|
| 9990 |
},
|
| 9991 |
+
"node_modules/proxy-addr": {
|
| 9992 |
+
"version": "2.0.7",
|
| 9993 |
+
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
| 9994 |
+
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
| 9995 |
+
"license": "MIT",
|
| 9996 |
+
"dependencies": {
|
| 9997 |
+
"forwarded": "0.2.0",
|
| 9998 |
+
"ipaddr.js": "1.9.1"
|
| 9999 |
+
},
|
| 10000 |
+
"engines": {
|
| 10001 |
+
"node": ">= 0.10"
|
| 10002 |
+
}
|
| 10003 |
+
},
|
| 10004 |
"node_modules/psl": {
|
| 10005 |
"version": "1.15.0",
|
| 10006 |
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
|
|
|
| 10043 |
"teleport": ">=0.2.0"
|
| 10044 |
}
|
| 10045 |
},
|
| 10046 |
+
"node_modules/qs": {
|
| 10047 |
+
"version": "6.14.0",
|
| 10048 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
| 10049 |
+
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
| 10050 |
+
"license": "BSD-3-Clause",
|
| 10051 |
+
"dependencies": {
|
| 10052 |
+
"side-channel": "^1.1.0"
|
| 10053 |
+
},
|
| 10054 |
+
"engines": {
|
| 10055 |
+
"node": ">=0.6"
|
| 10056 |
+
},
|
| 10057 |
+
"funding": {
|
| 10058 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 10059 |
+
}
|
| 10060 |
+
},
|
| 10061 |
"node_modules/quansync": {
|
| 10062 |
"version": "0.2.10",
|
| 10063 |
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
|
|
|
| 10107 |
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
| 10108 |
"license": "MIT"
|
| 10109 |
},
|
| 10110 |
+
"node_modules/range-parser": {
|
| 10111 |
+
"version": "1.2.1",
|
| 10112 |
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
| 10113 |
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
| 10114 |
+
"license": "MIT",
|
| 10115 |
+
"engines": {
|
| 10116 |
+
"node": ">= 0.6"
|
| 10117 |
+
}
|
| 10118 |
+
},
|
| 10119 |
+
"node_modules/raw-body": {
|
| 10120 |
+
"version": "3.0.1",
|
| 10121 |
+
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
| 10122 |
+
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
| 10123 |
+
"license": "MIT",
|
| 10124 |
+
"dependencies": {
|
| 10125 |
+
"bytes": "3.1.2",
|
| 10126 |
+
"http-errors": "2.0.0",
|
| 10127 |
+
"iconv-lite": "0.7.0",
|
| 10128 |
+
"unpipe": "1.0.0"
|
| 10129 |
+
},
|
| 10130 |
+
"engines": {
|
| 10131 |
+
"node": ">= 0.10"
|
| 10132 |
+
}
|
| 10133 |
+
},
|
| 10134 |
+
"node_modules/raw-body/node_modules/iconv-lite": {
|
| 10135 |
+
"version": "0.7.0",
|
| 10136 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
| 10137 |
+
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
| 10138 |
+
"license": "MIT",
|
| 10139 |
+
"dependencies": {
|
| 10140 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 10141 |
+
},
|
| 10142 |
+
"engines": {
|
| 10143 |
+
"node": ">=0.10.0"
|
| 10144 |
+
},
|
| 10145 |
+
"funding": {
|
| 10146 |
+
"type": "opencollective",
|
| 10147 |
+
"url": "https://opencollective.com/express"
|
| 10148 |
+
}
|
| 10149 |
+
},
|
| 10150 |
"node_modules/react-is": {
|
| 10151 |
"version": "17.0.2",
|
| 10152 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
|
|
|
| 10202 |
"node": ">= 12.13.0"
|
| 10203 |
}
|
| 10204 |
},
|
| 10205 |
+
"node_modules/require-from-string": {
|
| 10206 |
+
"version": "2.0.2",
|
| 10207 |
+
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
| 10208 |
+
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
| 10209 |
+
"license": "MIT",
|
| 10210 |
+
"engines": {
|
| 10211 |
+
"node": ">=0.10.0"
|
| 10212 |
+
}
|
| 10213 |
+
},
|
| 10214 |
"node_modules/requires-port": {
|
| 10215 |
"version": "1.0.0",
|
| 10216 |
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
|
|
|
| 10356 |
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
| 10357 |
"license": "MIT"
|
| 10358 |
},
|
| 10359 |
+
"node_modules/router": {
|
| 10360 |
+
"version": "2.2.0",
|
| 10361 |
+
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
| 10362 |
+
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
| 10363 |
+
"license": "MIT",
|
| 10364 |
+
"dependencies": {
|
| 10365 |
+
"debug": "^4.4.0",
|
| 10366 |
+
"depd": "^2.0.0",
|
| 10367 |
+
"is-promise": "^4.0.0",
|
| 10368 |
+
"parseurl": "^1.3.3",
|
| 10369 |
+
"path-to-regexp": "^8.0.0"
|
| 10370 |
+
},
|
| 10371 |
+
"engines": {
|
| 10372 |
+
"node": ">= 18"
|
| 10373 |
+
}
|
| 10374 |
+
},
|
| 10375 |
"node_modules/rrweb-cssom": {
|
| 10376 |
"version": "0.6.0",
|
| 10377 |
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
|
|
|
|
| 10533 |
"node": ">=10"
|
| 10534 |
}
|
| 10535 |
},
|
| 10536 |
+
"node_modules/send": {
|
| 10537 |
+
"version": "1.2.0",
|
| 10538 |
+
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
| 10539 |
+
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
| 10540 |
+
"license": "MIT",
|
| 10541 |
+
"dependencies": {
|
| 10542 |
+
"debug": "^4.3.5",
|
| 10543 |
+
"encodeurl": "^2.0.0",
|
| 10544 |
+
"escape-html": "^1.0.3",
|
| 10545 |
+
"etag": "^1.8.1",
|
| 10546 |
+
"fresh": "^2.0.0",
|
| 10547 |
+
"http-errors": "^2.0.0",
|
| 10548 |
+
"mime-types": "^3.0.1",
|
| 10549 |
+
"ms": "^2.1.3",
|
| 10550 |
+
"on-finished": "^2.4.1",
|
| 10551 |
+
"range-parser": "^1.2.1",
|
| 10552 |
+
"statuses": "^2.0.1"
|
| 10553 |
+
},
|
| 10554 |
+
"engines": {
|
| 10555 |
+
"node": ">= 18"
|
| 10556 |
+
}
|
| 10557 |
+
},
|
| 10558 |
+
"node_modules/send/node_modules/mime-db": {
|
| 10559 |
+
"version": "1.54.0",
|
| 10560 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
| 10561 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
| 10562 |
+
"license": "MIT",
|
| 10563 |
+
"engines": {
|
| 10564 |
+
"node": ">= 0.6"
|
| 10565 |
+
}
|
| 10566 |
+
},
|
| 10567 |
+
"node_modules/send/node_modules/mime-types": {
|
| 10568 |
+
"version": "3.0.1",
|
| 10569 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
| 10570 |
+
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
| 10571 |
+
"license": "MIT",
|
| 10572 |
+
"dependencies": {
|
| 10573 |
+
"mime-db": "^1.54.0"
|
| 10574 |
+
},
|
| 10575 |
+
"engines": {
|
| 10576 |
+
"node": ">= 0.6"
|
| 10577 |
+
}
|
| 10578 |
+
},
|
| 10579 |
+
"node_modules/serve-static": {
|
| 10580 |
+
"version": "2.2.0",
|
| 10581 |
+
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
| 10582 |
+
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
| 10583 |
+
"license": "MIT",
|
| 10584 |
+
"dependencies": {
|
| 10585 |
+
"encodeurl": "^2.0.0",
|
| 10586 |
+
"escape-html": "^1.0.3",
|
| 10587 |
+
"parseurl": "^1.3.3",
|
| 10588 |
+
"send": "^1.2.0"
|
| 10589 |
+
},
|
| 10590 |
+
"engines": {
|
| 10591 |
+
"node": ">= 18"
|
| 10592 |
+
}
|
| 10593 |
+
},
|
| 10594 |
"node_modules/set-cookie-parser": {
|
| 10595 |
"version": "2.7.1",
|
| 10596 |
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
|
|
|
| 10598 |
"devOptional": true,
|
| 10599 |
"license": "MIT"
|
| 10600 |
},
|
| 10601 |
+
"node_modules/setprototypeof": {
|
| 10602 |
+
"version": "1.2.0",
|
| 10603 |
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
| 10604 |
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
| 10605 |
+
"license": "ISC"
|
| 10606 |
+
},
|
| 10607 |
"node_modules/sharp": {
|
| 10608 |
"version": "0.33.5",
|
| 10609 |
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
|
|
|
| 10664 |
"node": ">=8"
|
| 10665 |
}
|
| 10666 |
},
|
| 10667 |
+
"node_modules/side-channel": {
|
| 10668 |
+
"version": "1.1.0",
|
| 10669 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
| 10670 |
+
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
| 10671 |
+
"license": "MIT",
|
| 10672 |
+
"dependencies": {
|
| 10673 |
+
"es-errors": "^1.3.0",
|
| 10674 |
+
"object-inspect": "^1.13.3",
|
| 10675 |
+
"side-channel-list": "^1.0.0",
|
| 10676 |
+
"side-channel-map": "^1.0.1",
|
| 10677 |
+
"side-channel-weakmap": "^1.0.2"
|
| 10678 |
+
},
|
| 10679 |
+
"engines": {
|
| 10680 |
+
"node": ">= 0.4"
|
| 10681 |
+
},
|
| 10682 |
+
"funding": {
|
| 10683 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 10684 |
+
}
|
| 10685 |
+
},
|
| 10686 |
+
"node_modules/side-channel-list": {
|
| 10687 |
+
"version": "1.0.0",
|
| 10688 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
| 10689 |
+
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
| 10690 |
+
"license": "MIT",
|
| 10691 |
+
"dependencies": {
|
| 10692 |
+
"es-errors": "^1.3.0",
|
| 10693 |
+
"object-inspect": "^1.13.3"
|
| 10694 |
+
},
|
| 10695 |
+
"engines": {
|
| 10696 |
+
"node": ">= 0.4"
|
| 10697 |
+
},
|
| 10698 |
+
"funding": {
|
| 10699 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 10700 |
+
}
|
| 10701 |
+
},
|
| 10702 |
+
"node_modules/side-channel-map": {
|
| 10703 |
+
"version": "1.0.1",
|
| 10704 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
| 10705 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
| 10706 |
+
"license": "MIT",
|
| 10707 |
+
"dependencies": {
|
| 10708 |
+
"call-bound": "^1.0.2",
|
| 10709 |
+
"es-errors": "^1.3.0",
|
| 10710 |
+
"get-intrinsic": "^1.2.5",
|
| 10711 |
+
"object-inspect": "^1.13.3"
|
| 10712 |
+
},
|
| 10713 |
+
"engines": {
|
| 10714 |
+
"node": ">= 0.4"
|
| 10715 |
+
},
|
| 10716 |
+
"funding": {
|
| 10717 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 10718 |
+
}
|
| 10719 |
+
},
|
| 10720 |
+
"node_modules/side-channel-weakmap": {
|
| 10721 |
+
"version": "1.0.2",
|
| 10722 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
| 10723 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
| 10724 |
+
"license": "MIT",
|
| 10725 |
+
"dependencies": {
|
| 10726 |
+
"call-bound": "^1.0.2",
|
| 10727 |
+
"es-errors": "^1.3.0",
|
| 10728 |
+
"get-intrinsic": "^1.2.5",
|
| 10729 |
+
"object-inspect": "^1.13.3",
|
| 10730 |
+
"side-channel-map": "^1.0.1"
|
| 10731 |
+
},
|
| 10732 |
+
"engines": {
|
| 10733 |
+
"node": ">= 0.4"
|
| 10734 |
+
},
|
| 10735 |
+
"funding": {
|
| 10736 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 10737 |
+
}
|
| 10738 |
+
},
|
| 10739 |
"node_modules/siginfo": {
|
| 10740 |
"version": "2.0.0",
|
| 10741 |
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
|
|
|
| 10903 |
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
| 10904 |
"license": "MIT"
|
| 10905 |
},
|
| 10906 |
+
"node_modules/statuses": {
|
| 10907 |
+
"version": "2.0.2",
|
| 10908 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
| 10909 |
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
| 10910 |
+
"license": "MIT",
|
| 10911 |
+
"engines": {
|
| 10912 |
+
"node": ">= 0.8"
|
| 10913 |
+
}
|
| 10914 |
+
},
|
| 10915 |
"node_modules/std-env": {
|
| 10916 |
"version": "3.9.0",
|
| 10917 |
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
|
|
|
|
| 11677 |
"node": ">=8.0"
|
| 11678 |
}
|
| 11679 |
},
|
| 11680 |
+
"node_modules/toidentifier": {
|
| 11681 |
+
"version": "1.0.1",
|
| 11682 |
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
| 11683 |
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
| 11684 |
+
"license": "MIT",
|
| 11685 |
+
"engines": {
|
| 11686 |
+
"node": ">=0.6"
|
| 11687 |
+
}
|
| 11688 |
+
},
|
| 11689 |
"node_modules/token-types": {
|
| 11690 |
"version": "6.0.0",
|
| 11691 |
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz",
|
|
|
|
| 11790 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 11791 |
}
|
| 11792 |
},
|
| 11793 |
+
"node_modules/type-is": {
|
| 11794 |
+
"version": "2.0.1",
|
| 11795 |
+
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
| 11796 |
+
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
| 11797 |
+
"license": "MIT",
|
| 11798 |
+
"dependencies": {
|
| 11799 |
+
"content-type": "^1.0.5",
|
| 11800 |
+
"media-typer": "^1.1.0",
|
| 11801 |
+
"mime-types": "^3.0.0"
|
| 11802 |
+
},
|
| 11803 |
+
"engines": {
|
| 11804 |
+
"node": ">= 0.6"
|
| 11805 |
+
}
|
| 11806 |
+
},
|
| 11807 |
+
"node_modules/type-is/node_modules/mime-db": {
|
| 11808 |
+
"version": "1.54.0",
|
| 11809 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
| 11810 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
| 11811 |
+
"license": "MIT",
|
| 11812 |
+
"engines": {
|
| 11813 |
+
"node": ">= 0.6"
|
| 11814 |
+
}
|
| 11815 |
+
},
|
| 11816 |
+
"node_modules/type-is/node_modules/mime-types": {
|
| 11817 |
+
"version": "3.0.1",
|
| 11818 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
| 11819 |
+
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
| 11820 |
+
"license": "MIT",
|
| 11821 |
+
"dependencies": {
|
| 11822 |
+
"mime-db": "^1.54.0"
|
| 11823 |
+
},
|
| 11824 |
+
"engines": {
|
| 11825 |
+
"node": ">= 0.6"
|
| 11826 |
+
}
|
| 11827 |
+
},
|
| 11828 |
"node_modules/typescript": {
|
| 11829 |
"version": "5.8.3",
|
| 11830 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
|
|
|
| 11910 |
"node": ">= 4.0.0"
|
| 11911 |
}
|
| 11912 |
},
|
| 11913 |
+
"node_modules/unpipe": {
|
| 11914 |
+
"version": "1.0.0",
|
| 11915 |
+
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
| 11916 |
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
| 11917 |
+
"license": "MIT",
|
| 11918 |
+
"engines": {
|
| 11919 |
+
"node": ">= 0.8"
|
| 11920 |
+
}
|
| 11921 |
+
},
|
| 11922 |
"node_modules/unplugin": {
|
| 11923 |
"version": "1.16.1",
|
| 11924 |
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
|
|
|
|
| 12051 |
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==",
|
| 12052 |
"license": "MIT"
|
| 12053 |
},
|
| 12054 |
+
"node_modules/vary": {
|
| 12055 |
+
"version": "1.1.2",
|
| 12056 |
+
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
| 12057 |
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
| 12058 |
+
"license": "MIT",
|
| 12059 |
+
"engines": {
|
| 12060 |
+
"node": ">= 0.8"
|
| 12061 |
+
}
|
| 12062 |
+
},
|
| 12063 |
"node_modules/vite": {
|
| 12064 |
"version": "6.3.5",
|
| 12065 |
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
|
|
|
| 12647 |
"funding": {
|
| 12648 |
"url": "https://github.com/sponsors/colinhacks"
|
| 12649 |
}
|
| 12650 |
+
},
|
| 12651 |
+
"node_modules/zod-to-json-schema": {
|
| 12652 |
+
"version": "3.24.6",
|
| 12653 |
+
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
| 12654 |
+
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
| 12655 |
+
"license": "ISC",
|
| 12656 |
+
"peerDependencies": {
|
| 12657 |
+
"zod": "^3.24.1"
|
| 12658 |
+
}
|
| 12659 |
}
|
| 12660 |
}
|
| 12661 |
}
|
|
@@ -70,6 +70,7 @@
|
|
| 70 |
"@elysiajs/swagger": "^1.3.0",
|
| 71 |
"@huggingface/hub": "^2.2.0",
|
| 72 |
"@huggingface/inference": "^4.11.3",
|
|
|
|
| 73 |
"@iconify-json/bi": "^1.1.21",
|
| 74 |
"@resvg/resvg-js": "^2.6.2",
|
| 75 |
"autoprefixer": "^10.4.14",
|
|
|
|
| 70 |
"@elysiajs/swagger": "^1.3.0",
|
| 71 |
"@huggingface/hub": "^2.2.0",
|
| 72 |
"@huggingface/inference": "^4.11.3",
|
| 73 |
+
"@modelcontextprotocol/sdk": "^1.21.1",
|
| 74 |
"@iconify-json/bi": "^1.1.21",
|
| 75 |
"@resvg/resvg-js": "^2.6.2",
|
| 76 |
"autoprefixer": "^10.4.14",
|
|
@@ -19,6 +19,7 @@ import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
|
|
| 19 |
import { adminTokenManager } from "$lib/server/adminToken";
|
| 20 |
import { isHostLocalhost } from "$lib/server/isURLLocal";
|
| 21 |
import { MetricsServer } from "$lib/server/metrics";
|
|
|
|
| 22 |
|
| 23 |
export const init: ServerInit = async () => {
|
| 24 |
// Wait for config to be fully loaded
|
|
@@ -49,6 +50,9 @@ export const init: ServerInit = async () => {
|
|
| 49 |
checkAndRunMigrations();
|
| 50 |
refreshConversationStats();
|
| 51 |
|
|
|
|
|
|
|
|
|
|
| 52 |
// Init AbortedGenerations refresh process
|
| 53 |
AbortedGenerations.getInstance();
|
| 54 |
|
|
|
|
| 19 |
import { adminTokenManager } from "$lib/server/adminToken";
|
| 20 |
import { isHostLocalhost } from "$lib/server/isURLLocal";
|
| 21 |
import { MetricsServer } from "$lib/server/metrics";
|
| 22 |
+
import { loadMcpServersOnStartup } from "$lib/server/mcp/registry";
|
| 23 |
|
| 24 |
export const init: ServerInit = async () => {
|
| 25 |
// Wait for config to be fully loaded
|
|
|
|
| 50 |
checkAndRunMigrations();
|
| 51 |
refreshConversationStats();
|
| 52 |
|
| 53 |
+
// Load MCP servers at startup
|
| 54 |
+
loadMcpServersOnStartup();
|
| 55 |
+
|
| 56 |
// Init AbortedGenerations refresh process
|
| 57 |
AbortedGenerations.getInstance();
|
| 58 |
|
|
@@ -28,6 +28,8 @@
|
|
| 28 |
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 29 |
import { useAPIClient, handleResponse } from "$lib/APIClient";
|
| 30 |
import { requireAuthUser } from "$lib/utils/auth";
|
|
|
|
|
|
|
| 31 |
|
| 32 |
const publicConfig = usePublicConfig();
|
| 33 |
const client = useAPIClient();
|
|
@@ -112,6 +114,7 @@
|
|
| 112 |
|
| 113 |
let isDark = $state(false);
|
| 114 |
let unsubscribeTheme: (() => void) | undefined;
|
|
|
|
| 115 |
|
| 116 |
if (browser) {
|
| 117 |
unsubscribeTheme = subscribeToTheme(({ isDark: nextIsDark }) => {
|
|
@@ -194,6 +197,22 @@
|
|
| 194 |
>
|
| 195 |
</a>
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
<span class="flex gap-1">
|
| 198 |
<a
|
| 199 |
href="{base}/settings/application"
|
|
@@ -219,3 +238,7 @@
|
|
| 219 |
</button>
|
| 220 |
</span>
|
| 221 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 29 |
import { useAPIClient, handleResponse } from "$lib/APIClient";
|
| 30 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 31 |
+
import { enabledServersCount } from "$lib/stores/mcpServers";
|
| 32 |
+
import MCPServerManager from "./mcp/MCPServerManager.svelte";
|
| 33 |
|
| 34 |
const publicConfig = usePublicConfig();
|
| 35 |
const client = useAPIClient();
|
|
|
|
| 114 |
|
| 115 |
let isDark = $state(false);
|
| 116 |
let unsubscribeTheme: (() => void) | undefined;
|
| 117 |
+
let showMcpModal = $state(false);
|
| 118 |
|
| 119 |
if (browser) {
|
| 120 |
unsubscribeTheme = subscribeToTheme(({ isDark: nextIsDark }) => {
|
|
|
|
| 197 |
>
|
| 198 |
</a>
|
| 199 |
|
| 200 |
+
{#if user?.username || user?.email}
|
| 201 |
+
<button
|
| 202 |
+
onclick={() => (showMcpModal = true)}
|
| 203 |
+
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
| 204 |
+
>
|
| 205 |
+
MCP Servers
|
| 206 |
+
{#if $enabledServersCount > 0}
|
| 207 |
+
<span
|
| 208 |
+
class="ml-auto rounded-md bg-blue-600/10 px-1.5 py-0.5 text-xs text-blue-600 dark:bg-blue-600/20 dark:text-blue-400"
|
| 209 |
+
>
|
| 210 |
+
{$enabledServersCount}
|
| 211 |
+
</span>
|
| 212 |
+
{/if}
|
| 213 |
+
</button>
|
| 214 |
+
{/if}
|
| 215 |
+
|
| 216 |
<span class="flex gap-1">
|
| 217 |
<a
|
| 218 |
href="{base}/settings/application"
|
|
|
|
| 238 |
</button>
|
| 239 |
</span>
|
| 240 |
</div>
|
| 241 |
+
|
| 242 |
+
{#if showMcpModal}
|
| 243 |
+
<MCPServerManager onclose={() => (showMcpModal = false)} />
|
| 244 |
+
{/if}
|
|
@@ -27,7 +27,7 @@
|
|
| 27 |
tabindex="0"
|
| 28 |
onclick={toggle}
|
| 29 |
onkeydown={onKeydown}
|
| 30 |
-
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 peer-checked:bg-
|
| 31 |
>
|
| 32 |
<div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-transform"></div>
|
| 33 |
</div>
|
|
|
|
| 27 |
tabindex="0"
|
| 28 |
onclick={toggle}
|
| 29 |
onkeydown={onKeydown}
|
| 30 |
+
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-gray-300 p-1 shadow-inner ring-gray-400 peer-checked:bg-blue-600 hover:bg-gray-400 peer-checked:hover:bg-blue-600 focus-visible:ring focus-visible:ring-offset-1 dark:bg-gray-600 dark:ring-gray-700 dark:hover:bg-gray-500 dark:peer-checked:hover:bg-blue-600 peer-checked:[&>div]:translate-x-3.5"
|
| 31 |
>
|
| 32 |
<div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-transform"></div>
|
| 33 |
</div>
|
|
@@ -10,11 +10,21 @@
|
|
| 10 |
import CarbonUpload from "~icons/carbon/upload";
|
| 11 |
import CarbonLink from "~icons/carbon/link";
|
| 12 |
import CarbonChevronRight from "~icons/carbon/chevron-right";
|
|
|
|
| 13 |
import UrlFetchModal from "./UrlFetchModal.svelte";
|
| 14 |
import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
|
|
|
|
|
|
|
| 15 |
|
| 16 |
import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
|
| 17 |
import { requireAuthUser } from "$lib/utils/auth";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
interface Props {
|
| 20 |
files?: File[];
|
|
@@ -25,6 +35,8 @@
|
|
| 25 |
disabled?: boolean;
|
| 26 |
// tools removed
|
| 27 |
modelIsMultimodal?: boolean;
|
|
|
|
|
|
|
| 28 |
children?: import("svelte").Snippet;
|
| 29 |
onPaste?: (e: ClipboardEvent) => void;
|
| 30 |
focused?: boolean;
|
|
@@ -40,6 +52,7 @@
|
|
| 40 |
disabled = false,
|
| 41 |
|
| 42 |
modelIsMultimodal = false,
|
|
|
|
| 43 |
children,
|
| 44 |
onPaste,
|
| 45 |
focused = $bindable(false),
|
|
@@ -62,6 +75,7 @@
|
|
| 62 |
|
| 63 |
let fileInputEl: HTMLInputElement | undefined = $state();
|
| 64 |
let isUrlModalOpen = $state(false);
|
|
|
|
| 65 |
|
| 66 |
function openPickerWithAccept(accept: string) {
|
| 67 |
if (!fileInputEl) return;
|
|
@@ -243,10 +257,13 @@
|
|
| 243 |
</DropdownMenu.Trigger>
|
| 244 |
<DropdownMenu.Portal>
|
| 245 |
<DropdownMenu.Content
|
| 246 |
-
class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur
|
| 247 |
side="top"
|
| 248 |
sideOffset={8}
|
| 249 |
align="start"
|
|
|
|
|
|
|
|
|
|
| 250 |
>
|
| 251 |
{#if modelIsMultimodal}
|
| 252 |
<DropdownMenu.Item
|
|
@@ -271,8 +288,11 @@
|
|
| 271 |
</div>
|
| 272 |
</DropdownMenu.SubTrigger>
|
| 273 |
<DropdownMenu.SubContent
|
| 274 |
-
class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur
|
| 275 |
sideOffset={10}
|
|
|
|
|
|
|
|
|
|
| 276 |
>
|
| 277 |
<DropdownMenu.Item
|
| 278 |
class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10"
|
|
@@ -290,9 +310,105 @@
|
|
| 290 |
</DropdownMenu.Item>
|
| 291 |
</DropdownMenu.SubContent>
|
| 292 |
</DropdownMenu.Sub>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
</DropdownMenu.Content>
|
| 294 |
</DropdownMenu.Portal>
|
| 295 |
</DropdownMenu.Root>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
</div>
|
| 297 |
{/if}
|
| 298 |
</div>
|
|
@@ -304,6 +420,10 @@
|
|
| 304 |
acceptMimeTypes={mimeTypes}
|
| 305 |
onfiles={handleFetchedFiles}
|
| 306 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
</div>
|
| 308 |
|
| 309 |
<style lang="postcss">
|
|
|
|
| 10 |
import CarbonUpload from "~icons/carbon/upload";
|
| 11 |
import CarbonLink from "~icons/carbon/link";
|
| 12 |
import CarbonChevronRight from "~icons/carbon/chevron-right";
|
| 13 |
+
import CarbonClose from "~icons/carbon/close";
|
| 14 |
import UrlFetchModal from "./UrlFetchModal.svelte";
|
| 15 |
import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
|
| 16 |
+
import MCPServerManager from "$lib/components/mcp/MCPServerManager.svelte";
|
| 17 |
+
import IconMCP from "$lib/components/icons/IconMCP.svelte";
|
| 18 |
|
| 19 |
import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
|
| 20 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 21 |
+
import {
|
| 22 |
+
enabledServersCount,
|
| 23 |
+
selectedServerIds,
|
| 24 |
+
allMcpServers,
|
| 25 |
+
toggleServer,
|
| 26 |
+
} from "$lib/stores/mcpServers";
|
| 27 |
+
import { getMcpServerFaviconUrl } from "$lib/utils/favicon";
|
| 28 |
|
| 29 |
interface Props {
|
| 30 |
files?: File[];
|
|
|
|
| 35 |
disabled?: boolean;
|
| 36 |
// tools removed
|
| 37 |
modelIsMultimodal?: boolean;
|
| 38 |
+
// Whether the currently selected model supports tool calling (incl. overrides)
|
| 39 |
+
modelSupportsTools?: boolean;
|
| 40 |
children?: import("svelte").Snippet;
|
| 41 |
onPaste?: (e: ClipboardEvent) => void;
|
| 42 |
focused?: boolean;
|
|
|
|
| 52 |
disabled = false,
|
| 53 |
|
| 54 |
modelIsMultimodal = false,
|
| 55 |
+
modelSupportsTools = true,
|
| 56 |
children,
|
| 57 |
onPaste,
|
| 58 |
focused = $bindable(false),
|
|
|
|
| 75 |
|
| 76 |
let fileInputEl: HTMLInputElement | undefined = $state();
|
| 77 |
let isUrlModalOpen = $state(false);
|
| 78 |
+
let isMcpManagerOpen = $state(false);
|
| 79 |
|
| 80 |
function openPickerWithAccept(accept: string) {
|
| 81 |
if (!fileInputEl) return;
|
|
|
|
| 257 |
</DropdownMenu.Trigger>
|
| 258 |
<DropdownMenu.Portal>
|
| 259 |
<DropdownMenu.Content
|
| 260 |
+
class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100"
|
| 261 |
side="top"
|
| 262 |
sideOffset={8}
|
| 263 |
align="start"
|
| 264 |
+
trapFocus={false}
|
| 265 |
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
| 266 |
+
interactOutsideBehavior="defer-otherwise-close"
|
| 267 |
>
|
| 268 |
{#if modelIsMultimodal}
|
| 269 |
<DropdownMenu.Item
|
|
|
|
| 288 |
</div>
|
| 289 |
</DropdownMenu.SubTrigger>
|
| 290 |
<DropdownMenu.SubContent
|
| 291 |
+
class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100"
|
| 292 |
sideOffset={10}
|
| 293 |
+
trapFocus={false}
|
| 294 |
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
| 295 |
+
interactOutsideBehavior="defer-otherwise-close"
|
| 296 |
>
|
| 297 |
<DropdownMenu.Item
|
| 298 |
class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10"
|
|
|
|
| 310 |
</DropdownMenu.Item>
|
| 311 |
</DropdownMenu.SubContent>
|
| 312 |
</DropdownMenu.Sub>
|
| 313 |
+
|
| 314 |
+
<!-- MCP Servers submenu -->
|
| 315 |
+
<DropdownMenu.Sub>
|
| 316 |
+
<DropdownMenu.SubTrigger
|
| 317 |
+
class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 data-[state=open]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 dark:data-[state=open]:bg-white/10"
|
| 318 |
+
>
|
| 319 |
+
<div class="flex items-center gap-1">
|
| 320 |
+
<IconMCP classNames="size-4 opacity-90 dark:opacity-80" />
|
| 321 |
+
MCP Servers
|
| 322 |
+
</div>
|
| 323 |
+
<div class="ml-auto flex items-center">
|
| 324 |
+
<CarbonChevronRight class="size-4 opacity-70 dark:opacity-80" />
|
| 325 |
+
</div>
|
| 326 |
+
</DropdownMenu.SubTrigger>
|
| 327 |
+
<DropdownMenu.SubContent
|
| 328 |
+
class="z-50 rounded-xl border border-gray-200 bg-white/95 p-1 text-gray-800 shadow-lg backdrop-blur dark:border-gray-700/60 dark:bg-gray-800/95 dark:text-gray-100"
|
| 329 |
+
sideOffset={10}
|
| 330 |
+
trapFocus={false}
|
| 331 |
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
| 332 |
+
interactOutsideBehavior="defer-otherwise-close"
|
| 333 |
+
>
|
| 334 |
+
{#each $allMcpServers as server (server.id)}
|
| 335 |
+
<DropdownMenu.CheckboxItem
|
| 336 |
+
checked={$selectedServerIds.has(server.id)}
|
| 337 |
+
onCheckedChange={() => toggleServer(server.id)}
|
| 338 |
+
closeOnSelect={false}
|
| 339 |
+
class="flex h-9 select-none items-center gap-2 rounded-md px-2 text-sm leading-none text-gray-800 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-100 dark:data-[highlighted]:bg-white/10"
|
| 340 |
+
>
|
| 341 |
+
{#snippet children({ checked })}
|
| 342 |
+
<img
|
| 343 |
+
src={getMcpServerFaviconUrl(server.url)}
|
| 344 |
+
alt=""
|
| 345 |
+
class="size-4 flex-shrink-0 rounded"
|
| 346 |
+
/>
|
| 347 |
+
<span class="max-w-52 truncate py-1">{server.name}</span>
|
| 348 |
+
<div class="ml-auto flex items-center">
|
| 349 |
+
<!-- Toggle visual -->
|
| 350 |
+
<span
|
| 351 |
+
class={[
|
| 352 |
+
"relative mt-px flex h-4 w-7 items-center self-center rounded-full transition-colors",
|
| 353 |
+
checked ? "bg-blue-600/80" : "bg-gray-300 dark:bg-gray-700",
|
| 354 |
+
]}
|
| 355 |
+
>
|
| 356 |
+
<span
|
| 357 |
+
class={[
|
| 358 |
+
"block size-3 translate-x-0.5 rounded-full bg-white shadow transition-transform",
|
| 359 |
+
checked ? "translate-x-[14px]" : "translate-x-0.5",
|
| 360 |
+
]}
|
| 361 |
+
></span>
|
| 362 |
+
</span>
|
| 363 |
+
</div>
|
| 364 |
+
{/snippet}
|
| 365 |
+
</DropdownMenu.CheckboxItem>
|
| 366 |
+
{/each}
|
| 367 |
+
|
| 368 |
+
{#if $allMcpServers.length > 0}
|
| 369 |
+
<DropdownMenu.Separator class="my-1 h-px bg-gray-200 dark:bg-gray-700/60" />
|
| 370 |
+
{/if}
|
| 371 |
+
<DropdownMenu.Item
|
| 372 |
+
class="flex h-8 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10"
|
| 373 |
+
onSelect={() => (isMcpManagerOpen = true)}
|
| 374 |
+
>
|
| 375 |
+
Manage MCP Servers
|
| 376 |
+
</DropdownMenu.Item>
|
| 377 |
+
</DropdownMenu.SubContent>
|
| 378 |
+
</DropdownMenu.Sub>
|
| 379 |
</DropdownMenu.Content>
|
| 380 |
</DropdownMenu.Portal>
|
| 381 |
</DropdownMenu.Root>
|
| 382 |
+
|
| 383 |
+
{#if $enabledServersCount > 0}
|
| 384 |
+
<div
|
| 385 |
+
class="ml-2 inline-flex h-7 items-center gap-1.5 rounded-full border border-blue-500/10 bg-blue-600/10 pl-3 pr-1 text-xs font-semibold text-blue-700 dark:bg-blue-600/20 dark:text-blue-400"
|
| 386 |
+
class:grayscale={!modelSupportsTools}
|
| 387 |
+
class:opacity-60={!modelSupportsTools}
|
| 388 |
+
class:cursor-help={!modelSupportsTools}
|
| 389 |
+
title={modelSupportsTools
|
| 390 |
+
? "MCP servers enabled"
|
| 391 |
+
: "Current model doesn’t support tools"}
|
| 392 |
+
>
|
| 393 |
+
<button
|
| 394 |
+
class="cursor-pointer select-none bg-transparent p-0 leading-none text-current focus:outline-none"
|
| 395 |
+
type="button"
|
| 396 |
+
title="Manage MCP Servers"
|
| 397 |
+
onclick={() => (isMcpManagerOpen = true)}
|
| 398 |
+
class:line-through={!modelSupportsTools}
|
| 399 |
+
>
|
| 400 |
+
MCP ({$enabledServersCount})
|
| 401 |
+
</button>
|
| 402 |
+
<button
|
| 403 |
+
class="grid size-5 place-items-center rounded-full bg-blue-600/15 text-blue-700 transition-colors hover:bg-blue-600/25 dark:bg-blue-600/25 dark:text-blue-300 dark:hover:bg-blue-600/35"
|
| 404 |
+
aria-label="Disable all MCP servers"
|
| 405 |
+
onclick={() => selectedServerIds.set(new Set())}
|
| 406 |
+
type="button"
|
| 407 |
+
>
|
| 408 |
+
<CarbonClose class="size-3.5" />
|
| 409 |
+
</button>
|
| 410 |
+
</div>
|
| 411 |
+
{/if}
|
| 412 |
</div>
|
| 413 |
{/if}
|
| 414 |
</div>
|
|
|
|
| 420 |
acceptMimeTypes={mimeTypes}
|
| 421 |
onfiles={handleFetchedFiles}
|
| 422 |
/>
|
| 423 |
+
|
| 424 |
+
{#if isMcpManagerOpen}
|
| 425 |
+
<MCPServerManager onclose={() => (isMcpManagerOpen = false)} />
|
| 426 |
+
{/if}
|
| 427 |
</div>
|
| 428 |
|
| 429 |
<style lang="postcss">
|
|
@@ -18,6 +18,8 @@
|
|
| 18 |
import MessageAvatar from "./MessageAvatar.svelte";
|
| 19 |
import { PROVIDERS_HUB_ORGS } from "@huggingface/inference";
|
| 20 |
import { requireAuthUser } from "$lib/utils/auth";
|
|
|
|
|
|
|
| 21 |
|
| 22 |
interface Props {
|
| 23 |
message: Message;
|
|
@@ -77,6 +79,41 @@
|
|
| 77 |
message.content.replace(THINK_BLOCK_REGEX, "").trim()
|
| 78 |
);
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
$effect(() => {
|
| 81 |
if (isCopied) {
|
| 82 |
setTimeout(() => {
|
|
@@ -125,6 +162,27 @@
|
|
| 125 |
</div>
|
| 126 |
{/if}
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
<div bind:this={contentEl}>
|
| 129 |
{#if isLast && loading && message.content.length === 0}
|
| 130 |
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
|
@@ -148,7 +206,7 @@
|
|
| 148 |
/>
|
| 149 |
{:else if part && part.trim().length > 0}
|
| 150 |
<div
|
| 151 |
-
class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
|
| 152 |
>
|
| 153 |
<MarkdownRenderer content={part} loading={isLast && loading} />
|
| 154 |
</div>
|
|
@@ -156,7 +214,7 @@
|
|
| 156 |
{/each}
|
| 157 |
{:else}
|
| 158 |
<div
|
| 159 |
-
class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
|
| 160 |
>
|
| 161 |
<MarkdownRenderer content={message.content} loading={isLast && loading} />
|
| 162 |
</div>
|
|
|
|
| 18 |
import MessageAvatar from "./MessageAvatar.svelte";
|
| 19 |
import { PROVIDERS_HUB_ORGS } from "@huggingface/inference";
|
| 20 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 21 |
+
import ToolUpdate from "./ToolUpdate.svelte";
|
| 22 |
+
import { isMessageToolUpdate } from "$lib/utils/messageUpdates";
|
| 23 |
|
| 24 |
interface Props {
|
| 25 |
message: Message;
|
|
|
|
| 79 |
message.content.replace(THINK_BLOCK_REGEX, "").trim()
|
| 80 |
);
|
| 81 |
|
| 82 |
+
// Group tool updates (if any) by uuid for display
|
| 83 |
+
let toolUpdateGroups = $derived.by(() => {
|
| 84 |
+
const groups: Record<string, import("$lib/types/MessageUpdate").MessageToolUpdate[]> = {};
|
| 85 |
+
for (const u of message.updates ?? []) {
|
| 86 |
+
if (!isMessageToolUpdate(u)) continue;
|
| 87 |
+
(groups[u.uuid] ||= []).push(u);
|
| 88 |
+
}
|
| 89 |
+
return groups;
|
| 90 |
+
});
|
| 91 |
+
let hasToolUpdates = $derived(Object.keys(toolUpdateGroups).length > 0);
|
| 92 |
+
|
| 93 |
+
// Flatten to ordered array and keep a navigation index (defaults to last)
|
| 94 |
+
let toolGroups = $derived(Object.values(toolUpdateGroups));
|
| 95 |
+
let toolNavIndex = $state(0);
|
| 96 |
+
// Auto-follow newest tool group while streaming until user navigates manually
|
| 97 |
+
let toolAutoFollowLatest = $state(true);
|
| 98 |
+
$effect(() => {
|
| 99 |
+
const len = toolGroups.length;
|
| 100 |
+
if (len === 0) {
|
| 101 |
+
toolNavIndex = 0;
|
| 102 |
+
return;
|
| 103 |
+
}
|
| 104 |
+
// Clamp if groups shrink or grow
|
| 105 |
+
if (toolNavIndex > len - 1) toolNavIndex = len - 1;
|
| 106 |
+
// While streaming, default to most recent group unless user navigated away
|
| 107 |
+
if (isLast && loading && toolAutoFollowLatest) toolNavIndex = len - 1;
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// When streaming ends, re-enable auto-follow for the next turn
|
| 111 |
+
$effect(() => {
|
| 112 |
+
if (!loading) {
|
| 113 |
+
toolAutoFollowLatest = true;
|
| 114 |
+
}
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
$effect(() => {
|
| 118 |
if (isCopied) {
|
| 119 |
setTimeout(() => {
|
|
|
|
| 162 |
</div>
|
| 163 |
{/if}
|
| 164 |
|
| 165 |
+
{#if hasToolUpdates}
|
| 166 |
+
{#if toolGroups.length}
|
| 167 |
+
{@const group = toolGroups[toolNavIndex]}
|
| 168 |
+
<ToolUpdate
|
| 169 |
+
tool={group}
|
| 170 |
+
{loading}
|
| 171 |
+
index={toolNavIndex}
|
| 172 |
+
total={toolGroups.length}
|
| 173 |
+
onprev={() => {
|
| 174 |
+
toolAutoFollowLatest = false;
|
| 175 |
+
toolNavIndex = Math.max(0, toolNavIndex - 1);
|
| 176 |
+
}}
|
| 177 |
+
onnext={() => {
|
| 178 |
+
toolNavIndex = Math.min(toolGroups.length - 1, toolNavIndex + 1);
|
| 179 |
+
// If user moves back to the newest group, resume auto-follow
|
| 180 |
+
toolAutoFollowLatest = toolNavIndex === toolGroups.length - 1;
|
| 181 |
+
}}
|
| 182 |
+
/>
|
| 183 |
+
{/if}
|
| 184 |
+
{/if}
|
| 185 |
+
|
| 186 |
<div bind:this={contentEl}>
|
| 187 |
{#if isLast && loading && message.content.length === 0}
|
| 188 |
<IconLoading classNames="loading inline ml-2 first:ml-0" />
|
|
|
|
| 206 |
/>
|
| 207 |
{:else if part && part.trim().length > 0}
|
| 208 |
<div
|
| 209 |
+
class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:rounded-lg dark:prose-pre:bg-gray-900"
|
| 210 |
>
|
| 211 |
<MarkdownRenderer content={part} loading={isLast && loading} />
|
| 212 |
</div>
|
|
|
|
| 214 |
{/each}
|
| 215 |
{:else}
|
| 216 |
<div
|
| 217 |
+
class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 prose-img:my-0 prose-img:rounded-lg dark:prose-pre:bg-gray-900"
|
| 218 |
>
|
| 219 |
<MarkdownRenderer content={message.content} loading={isLast && loading} />
|
| 220 |
</div>
|
|
@@ -27,12 +27,20 @@
|
|
| 27 |
import { routerExamples } from "$lib/constants/routerExamples";
|
| 28 |
import type { RouterFollowUp, RouterExample } from "$lib/constants/routerExamples";
|
| 29 |
import { shareModal } from "$lib/stores/shareModal";
|
|
|
|
| 30 |
|
| 31 |
import { fly } from "svelte/transition";
|
| 32 |
import { cubicInOut } from "svelte/easing";
|
| 33 |
|
| 34 |
import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
|
| 35 |
import { requireAuthUser } from "$lib/utils/auth";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
interface Props {
|
| 38 |
messages?: Message[];
|
|
@@ -163,6 +171,26 @@
|
|
| 163 |
? (streamingRouterMetadata.model.split("/").pop() ?? streamingRouterMetadata.model)
|
| 164 |
: ""
|
| 165 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
let showRouterDetails = $state(false);
|
| 167 |
let routerDetailsTimeout: ReturnType<typeof setTimeout> | undefined;
|
| 168 |
|
|
@@ -231,6 +259,12 @@
|
|
| 231 |
let modelIsMultimodalOverride = $derived($settings.multimodalOverrides?.[currentModel.id]);
|
| 232 |
let modelIsMultimodal = $derived((modelIsMultimodalOverride ?? currentModel.multimodal) === true);
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
// Always allow common text-like files; add images only when model is multimodal
|
| 235 |
import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
|
| 236 |
|
|
@@ -492,6 +526,7 @@
|
|
| 492 |
{onPaste}
|
| 493 |
disabled={isReadOnly || lastIsError}
|
| 494 |
{modelIsMultimodal}
|
|
|
|
| 495 |
bind:focused
|
| 496 |
/>
|
| 497 |
{/if}
|
|
@@ -539,7 +574,16 @@
|
|
| 539 |
}}
|
| 540 |
>
|
| 541 |
{#if models.find((m) => m.id === currentModel.id)}
|
| 542 |
-
{#if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
<a
|
| 544 |
href="{base}/settings/{currentModel.id}"
|
| 545 |
onclick={(e) => {
|
|
@@ -557,7 +601,7 @@
|
|
| 557 |
{/if}
|
| 558 |
<CarbonCaretDown class="-ml-0.5 text-xxs" />
|
| 559 |
</a>
|
| 560 |
-
{:else if showRouterDetails && streamingRouterMetadata}
|
| 561 |
<div
|
| 562 |
class="mr-2 flex items-center gap-1.5 whitespace-nowrap text-[.70rem] text-xs leading-none text-gray-400 dark:text-gray-400"
|
| 563 |
>
|
|
|
|
| 27 |
import { routerExamples } from "$lib/constants/routerExamples";
|
| 28 |
import type { RouterFollowUp, RouterExample } from "$lib/constants/routerExamples";
|
| 29 |
import { shareModal } from "$lib/stores/shareModal";
|
| 30 |
+
import CarbonTools from "~icons/carbon/tools";
|
| 31 |
|
| 32 |
import { fly } from "svelte/transition";
|
| 33 |
import { cubicInOut } from "svelte/easing";
|
| 34 |
|
| 35 |
import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
|
| 36 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 37 |
+
import { page } from "$app/state";
|
| 38 |
+
import {
|
| 39 |
+
isMessageToolCallUpdate,
|
| 40 |
+
isMessageToolErrorUpdate,
|
| 41 |
+
isMessageToolResultUpdate,
|
| 42 |
+
} from "$lib/utils/messageUpdates";
|
| 43 |
+
import type { ToolFront } from "$lib/types/Tool";
|
| 44 |
|
| 45 |
interface Props {
|
| 46 |
messages?: Message[];
|
|
|
|
| 171 |
? (streamingRouterMetadata.model.split("/").pop() ?? streamingRouterMetadata.model)
|
| 172 |
: ""
|
| 173 |
);
|
| 174 |
+
|
| 175 |
+
// Expose currently running tool call name (if any) from the streaming assistant message
|
| 176 |
+
const availableTools: ToolFront[] = $derived.by(
|
| 177 |
+
() => (page.data as { tools?: ToolFront[] } | undefined)?.tools ?? []
|
| 178 |
+
);
|
| 179 |
+
let streamingToolCallName = $derived.by(() => {
|
| 180 |
+
const updates = streamingAssistantMessage?.updates ?? [];
|
| 181 |
+
if (!updates.length) return null;
|
| 182 |
+
const done = new Set<string>();
|
| 183 |
+
for (const u of updates) {
|
| 184 |
+
if (isMessageToolResultUpdate(u) || isMessageToolErrorUpdate(u)) done.add(u.uuid);
|
| 185 |
+
}
|
| 186 |
+
for (let i = updates.length - 1; i >= 0; i -= 1) {
|
| 187 |
+
const u = updates[i];
|
| 188 |
+
if (isMessageToolCallUpdate(u) && !done.has(u.uuid)) {
|
| 189 |
+
return u.call.name;
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
return null;
|
| 193 |
+
});
|
| 194 |
let showRouterDetails = $state(false);
|
| 195 |
let routerDetailsTimeout: ReturnType<typeof setTimeout> | undefined;
|
| 196 |
|
|
|
|
| 259 |
let modelIsMultimodalOverride = $derived($settings.multimodalOverrides?.[currentModel.id]);
|
| 260 |
let modelIsMultimodal = $derived((modelIsMultimodalOverride ?? currentModel.multimodal) === true);
|
| 261 |
|
| 262 |
+
// Determine tool support for the current model (server-provided capability with user override)
|
| 263 |
+
let modelSupportsTools = $derived(
|
| 264 |
+
($settings.toolsOverrides?.[currentModel.id] ??
|
| 265 |
+
(currentModel as unknown as { supportsTools?: boolean }).supportsTools) === true
|
| 266 |
+
);
|
| 267 |
+
|
| 268 |
// Always allow common text-like files; add images only when model is multimodal
|
| 269 |
import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime";
|
| 270 |
|
|
|
|
| 526 |
{onPaste}
|
| 527 |
disabled={isReadOnly || lastIsError}
|
| 528 |
{modelIsMultimodal}
|
| 529 |
+
{modelSupportsTools}
|
| 530 |
bind:focused
|
| 531 |
/>
|
| 532 |
{/if}
|
|
|
|
| 574 |
}}
|
| 575 |
>
|
| 576 |
{#if models.find((m) => m.id === currentModel.id)}
|
| 577 |
+
{#if loading && streamingToolCallName}
|
| 578 |
+
<span class="inline-flex items-center gap-1 whitespace-nowrap text-xs">
|
| 579 |
+
<CarbonTools class="text-[11px]" />
|
| 580 |
+
Calling tool
|
| 581 |
+
<span class="loading-dots font-medium">
|
| 582 |
+
{availableTools.find((t) => t.name === streamingToolCallName)?.displayName ??
|
| 583 |
+
streamingToolCallName}
|
| 584 |
+
</span>
|
| 585 |
+
</span>
|
| 586 |
+
{:else if !currentModel.isRouter || !loading}
|
| 587 |
<a
|
| 588 |
href="{base}/settings/{currentModel.id}"
|
| 589 |
onclick={(e) => {
|
|
|
|
| 601 |
{/if}
|
| 602 |
<CarbonCaretDown class="-ml-0.5 text-xxs" />
|
| 603 |
</a>
|
| 604 |
+
{:else if showRouterDetails && streamingRouterMetadata?.route}
|
| 605 |
<div
|
| 606 |
class="mr-2 flex items-center gap-1.5 whitespace-nowrap text-[.70rem] text-xs leading-none text-gray-400 dark:text-gray-400"
|
| 607 |
>
|
|
@@ -84,7 +84,7 @@
|
|
| 84 |
e.preventDefault();
|
| 85 |
}}
|
| 86 |
class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner
|
| 87 |
-
? 'border-blue-200 !bg-blue-
|
| 88 |
: 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
|
| 89 |
>
|
| 90 |
<CarbonImage class="text-xl" />
|
|
|
|
| 84 |
e.preventDefault();
|
| 85 |
}}
|
| 86 |
class="relative flex h-28 w-full max-w-4xl flex-col items-center justify-center gap-1 rounded-xl border-2 border-dotted {onDragInner
|
| 87 |
+
? 'border-blue-200 !bg-blue-600/10 text-blue-600 *:pointer-events-none dark:border-blue-600 dark:bg-blue-600/20 dark:text-blue-600'
|
| 88 |
: 'bg-gray-100 text-gray-500 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-400'}"
|
| 89 |
>
|
| 90 |
<CarbonImage class="text-xl" />
|
|
@@ -29,60 +29,6 @@ describe("MarkdownRenderer", () => {
|
|
| 29 |
render(MarkdownRenderer, { content: "```foobar```" });
|
| 30 |
expect(page.getByRole("code")).toHaveTextContent("foobar");
|
| 31 |
});
|
| 32 |
-
it("renders sources correctly", () => {
|
| 33 |
-
const props = {
|
| 34 |
-
content: "Hello there [1]",
|
| 35 |
-
sources: [
|
| 36 |
-
{
|
| 37 |
-
title: "foo",
|
| 38 |
-
link: "https://example.com",
|
| 39 |
-
},
|
| 40 |
-
],
|
| 41 |
-
};
|
| 42 |
-
render(MarkdownRenderer, props);
|
| 43 |
-
|
| 44 |
-
const link = page.getByRole("link");
|
| 45 |
-
expect(link).toBeInTheDocument();
|
| 46 |
-
expect(link).toHaveAttribute("href", "https://example.com");
|
| 47 |
-
expect(link).toHaveAttribute("target", "_blank");
|
| 48 |
-
expect(link).toHaveAttribute("rel", "noreferrer");
|
| 49 |
-
});
|
| 50 |
-
it("handles groups of sources", () => {
|
| 51 |
-
render(MarkdownRenderer, {
|
| 52 |
-
content: "Hello there [1], [2], [3]",
|
| 53 |
-
sources: [
|
| 54 |
-
{
|
| 55 |
-
title: "foo",
|
| 56 |
-
link: "https://foo.com",
|
| 57 |
-
},
|
| 58 |
-
{
|
| 59 |
-
title: "bar",
|
| 60 |
-
link: "https://bar.com",
|
| 61 |
-
},
|
| 62 |
-
{
|
| 63 |
-
title: "baz",
|
| 64 |
-
link: "https://baz.com",
|
| 65 |
-
},
|
| 66 |
-
],
|
| 67 |
-
});
|
| 68 |
-
expect(page.getByRole("link").all()).toHaveLength(3);
|
| 69 |
-
expect(page.getByRole("link").nth(0)).toHaveAttribute("href", "https://foo.com");
|
| 70 |
-
expect(page.getByRole("link").nth(1)).toHaveAttribute("href", "https://bar.com");
|
| 71 |
-
expect(page.getByRole("link").nth(2)).toHaveAttribute("href", "https://baz.com");
|
| 72 |
-
});
|
| 73 |
-
it("does not render sources in code blocks", () => {
|
| 74 |
-
render(MarkdownRenderer, {
|
| 75 |
-
content: "```\narray[1]\n```",
|
| 76 |
-
sources: [
|
| 77 |
-
{
|
| 78 |
-
title: "foo",
|
| 79 |
-
link: "https://example.com",
|
| 80 |
-
},
|
| 81 |
-
],
|
| 82 |
-
});
|
| 83 |
-
const linkSelector = page.getByRole("link");
|
| 84 |
-
expect(linkSelector.elements).toHaveLength(0);
|
| 85 |
-
});
|
| 86 |
it("doesnt render raw html directly", () => {
|
| 87 |
render(MarkdownRenderer, { content: "<button>Click me</button>" });
|
| 88 |
expect(page.getByRole("button").elements).toHaveLength(0);
|
|
|
|
| 29 |
render(MarkdownRenderer, { content: "```foobar```" });
|
| 30 |
expect(page.getByRole("code")).toHaveTextContent("foobar");
|
| 31 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
it("doesnt render raw html directly", () => {
|
| 33 |
render(MarkdownRenderer, { content: "<button>Click me</button>" });
|
| 34 |
expect(page.getByRole("button").elements).toHaveLength(0);
|
|
@@ -18,7 +18,7 @@
|
|
| 18 |
|
| 19 |
<details
|
| 20 |
bind:open={isOpen}
|
| 21 |
-
class="group flex w-fit max-w-full flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 [&:has(+_.prose)]:mb-4"
|
| 22 |
>
|
| 23 |
<summary
|
| 24 |
class="
|
|
@@ -71,7 +71,7 @@
|
|
| 71 |
</summary>
|
| 72 |
|
| 73 |
<div
|
| 74 |
-
class="prose prose-sm !max-w-none space-y-4 border-t border-gray-200 p-3 text-sm text-gray-600 dark:prose-invert dark:border-gray-800 dark:text-gray-400"
|
| 75 |
>
|
| 76 |
{#key content}
|
| 77 |
<MarkdownRenderer {content} {loading} />
|
|
|
|
| 18 |
|
| 19 |
<details
|
| 20 |
bind:open={isOpen}
|
| 21 |
+
class="group flex w-fit max-w-full flex-col rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 [&:has(+_.prose)]:mb-4 [.prose+&]:mt-4 [details+&]:mt-2"
|
| 22 |
>
|
| 23 |
<summary
|
| 24 |
class="
|
|
|
|
| 71 |
</summary>
|
| 72 |
|
| 73 |
<div
|
| 74 |
+
class="prose prose-sm !max-w-none space-y-4 border-t border-gray-200 p-3 text-sm text-gray-600 dark:prose-invert prose-img:my-0 prose-img:rounded-lg dark:border-gray-800 dark:text-gray-400"
|
| 75 |
>
|
| 76 |
{#key content}
|
| 77 |
<MarkdownRenderer {content} {loading} />
|
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { MessageToolUpdateType, type MessageToolUpdate } from "$lib/types/MessageUpdate";
|
| 3 |
+
import {
|
| 4 |
+
isMessageToolCallUpdate,
|
| 5 |
+
isMessageToolErrorUpdate,
|
| 6 |
+
isMessageToolResultUpdate,
|
| 7 |
+
} from "$lib/utils/messageUpdates";
|
| 8 |
+
import CarbonTools from "~icons/carbon/tools";
|
| 9 |
+
import { ToolResultStatus, type ToolFront } from "$lib/types/Tool";
|
| 10 |
+
import { page } from "$app/state";
|
| 11 |
+
import { onDestroy } from "svelte";
|
| 12 |
+
import { browser } from "$app/environment";
|
| 13 |
+
import CarbonChevronLeft from "~icons/carbon/chevron-left";
|
| 14 |
+
import CarbonChevronRight from "~icons/carbon/chevron-right";
|
| 15 |
+
|
| 16 |
+
interface Props {
|
| 17 |
+
tool: MessageToolUpdate[];
|
| 18 |
+
loading?: boolean;
|
| 19 |
+
// Optional navigation props when multiple tool groups exist
|
| 20 |
+
index?: number;
|
| 21 |
+
total?: number;
|
| 22 |
+
onprev?: () => void;
|
| 23 |
+
onnext?: () => void;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
let { tool, loading = false, index, total, onprev, onnext }: Props = $props();
|
| 27 |
+
|
| 28 |
+
let toolFnName = $derived(tool.find(isMessageToolCallUpdate)?.call.name);
|
| 29 |
+
let toolError = $derived(tool.some(isMessageToolErrorUpdate));
|
| 30 |
+
let toolDone = $derived(tool.some(isMessageToolResultUpdate));
|
| 31 |
+
let eta = $derived(tool.find((update) => update.subtype === MessageToolUpdateType.ETA)?.eta);
|
| 32 |
+
|
| 33 |
+
const availableTools: ToolFront[] = $derived.by(
|
| 34 |
+
() => (page.data as { tools?: ToolFront[] } | undefined)?.tools ?? []
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
let loadingBarEl: HTMLDivElement | undefined = $state(undefined);
|
| 38 |
+
let animation: Animation | undefined = $state(undefined);
|
| 39 |
+
let showingLoadingBar = $state(false);
|
| 40 |
+
|
| 41 |
+
const formatValue = (value: unknown): string => {
|
| 42 |
+
if (value == null) return "";
|
| 43 |
+
if (typeof value === "object") {
|
| 44 |
+
try {
|
| 45 |
+
return JSON.stringify(value, null, 2);
|
| 46 |
+
} catch {
|
| 47 |
+
return String(value);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
return String(value);
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
$effect(() => {
|
| 54 |
+
if (!toolError && !toolDone && loading && loadingBarEl && eta) {
|
| 55 |
+
loadingBarEl.classList.remove("hidden");
|
| 56 |
+
showingLoadingBar = true;
|
| 57 |
+
animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], {
|
| 58 |
+
duration: (eta ?? 0) * 1000,
|
| 59 |
+
fill: "forwards",
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
onDestroy(() => {
|
| 65 |
+
animation?.cancel();
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
$effect(() => {
|
| 69 |
+
if ((!loading || toolDone || toolError) && browser && loadingBarEl && showingLoadingBar) {
|
| 70 |
+
showingLoadingBar = false;
|
| 71 |
+
loadingBarEl.classList.remove("hidden");
|
| 72 |
+
animation?.cancel();
|
| 73 |
+
const fromWidth = getComputedStyle(loadingBarEl).width;
|
| 74 |
+
animation = loadingBarEl.animate([{ width: fromWidth }, { width: "calc(100%+1rem)" }], {
|
| 75 |
+
duration: 300,
|
| 76 |
+
fill: "forwards",
|
| 77 |
+
});
|
| 78 |
+
setTimeout(() => loadingBarEl?.classList.add("hidden"), 300);
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
</script>
|
| 82 |
+
|
| 83 |
+
{#if toolFnName}
|
| 84 |
+
<details
|
| 85 |
+
class="group/tool my-2.5 w-fit max-w-full cursor-pointer rounded-lg border border-gray-200 bg-white px-1 {(total ??
|
| 86 |
+
0) > 1
|
| 87 |
+
? ''
|
| 88 |
+
: 'pr-2'} text-sm shadow-sm first:mt-0 open:mb-3 open:border-purple-500/10 open:bg-purple-600/5 open:shadow-sm dark:border-gray-800 dark:bg-gray-900 open:dark:border-purple-800/40 open:dark:bg-purple-800/10 [&+details]:-mt-2"
|
| 89 |
+
>
|
| 90 |
+
<summary
|
| 91 |
+
class="relative flex select-none list-none items-center gap-1.5 py-1 group-open/tool:text-purple-700 group-open/tool:dark:text-purple-300"
|
| 92 |
+
>
|
| 93 |
+
<div
|
| 94 |
+
bind:this={loadingBarEl}
|
| 95 |
+
class="absolute -m-1 hidden h-full w-full rounded-lg bg-purple-500/5 transition-all dark:bg-purple-500/10"
|
| 96 |
+
></div>
|
| 97 |
+
|
| 98 |
+
<div
|
| 99 |
+
class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
|
| 100 |
+
>
|
| 101 |
+
<svg
|
| 102 |
+
class="absolute inset-0 text-purple-500/40 transition-opacity"
|
| 103 |
+
class:invisible={toolDone || toolError}
|
| 104 |
+
width="22"
|
| 105 |
+
height="22"
|
| 106 |
+
viewBox="0 0 38 38"
|
| 107 |
+
fill="none"
|
| 108 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 109 |
+
>
|
| 110 |
+
<path
|
| 111 |
+
class="loading-path"
|
| 112 |
+
d="M8 2.5H30C30 2.5 35.5 2.5 35.5 8V30C35.5 30 35.5 35.5 30 35.5H8C8 35.5 2.5 35.5 2.5 30V8C2.5 8 2.5 2.5 8 2.5Z"
|
| 113 |
+
pathLength="100"
|
| 114 |
+
stroke="currentColor"
|
| 115 |
+
stroke-width="1"
|
| 116 |
+
stroke-linecap="round"
|
| 117 |
+
id="shape"
|
| 118 |
+
/>
|
| 119 |
+
</svg>
|
| 120 |
+
<CarbonTools class="text-xs text-purple-700 dark:text-purple-500" />
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<span class="relative">
|
| 124 |
+
{toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool
|
| 125 |
+
<span class="font-semibold"
|
| 126 |
+
>{availableTools.find((entry) => entry.name === toolFnName)?.displayName ??
|
| 127 |
+
toolFnName}</span
|
| 128 |
+
>
|
| 129 |
+
</span>
|
| 130 |
+
|
| 131 |
+
{#if (total ?? 0) > 1}
|
| 132 |
+
<div class="relative ml-auto flex items-center gap-1.5">
|
| 133 |
+
<div
|
| 134 |
+
class="flex items-center divide-x rounded-md border border-gray-200 bg-gray-50 dark:divide-gray-700 dark:border-gray-800 dark:bg-gray-800"
|
| 135 |
+
>
|
| 136 |
+
<button
|
| 137 |
+
type="button"
|
| 138 |
+
class="btn size-5 text-xs text-gray-500 hover:text-gray-700 focus:ring-0 disabled:opacity-40 dark:text-gray-400 dark:hover:text-gray-200"
|
| 139 |
+
title="Previous tool"
|
| 140 |
+
aria-label="Previous tool"
|
| 141 |
+
disabled={(index ?? 0) <= 0}
|
| 142 |
+
onclick={(e) => {
|
| 143 |
+
e.preventDefault();
|
| 144 |
+
e.stopPropagation();
|
| 145 |
+
onprev?.();
|
| 146 |
+
}}
|
| 147 |
+
>
|
| 148 |
+
<CarbonChevronLeft />
|
| 149 |
+
</button>
|
| 150 |
+
|
| 151 |
+
<span
|
| 152 |
+
class="select-none px-1 text-center text-[10px] font-medium text-gray-500 dark:text-gray-400"
|
| 153 |
+
aria-live="polite"
|
| 154 |
+
>
|
| 155 |
+
{(index ?? 0) + 1} <span class="text-gray-300 dark:text-gray-500">/</span>
|
| 156 |
+
{total}
|
| 157 |
+
</span>
|
| 158 |
+
<button
|
| 159 |
+
type="button"
|
| 160 |
+
class="btn size-5 text-xs text-gray-500 hover:text-gray-700 focus:ring-0 disabled:opacity-40 dark:text-gray-400 dark:hover:text-gray-200"
|
| 161 |
+
title="Next tool"
|
| 162 |
+
aria-label="Next tool"
|
| 163 |
+
disabled={(index ?? 0) >= (total ?? 1) - 1}
|
| 164 |
+
onclick={(e) => {
|
| 165 |
+
e.preventDefault();
|
| 166 |
+
e.stopPropagation();
|
| 167 |
+
onnext?.();
|
| 168 |
+
}}
|
| 169 |
+
>
|
| 170 |
+
<CarbonChevronRight />
|
| 171 |
+
</button>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
{/if}
|
| 175 |
+
</summary>
|
| 176 |
+
|
| 177 |
+
{#each tool as update}
|
| 178 |
+
{#if update.subtype === MessageToolUpdateType.Call}
|
| 179 |
+
<div class="mt-1 flex items-center gap-2 opacity-80">
|
| 180 |
+
<h3 class="text-sm">Parameters</h3>
|
| 181 |
+
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
|
| 182 |
+
</div>
|
| 183 |
+
<ul class="py-1 text-sm">
|
| 184 |
+
{#each Object.entries(update.call.parameters ?? {}) as [key, value]}
|
| 185 |
+
{#if value != null}
|
| 186 |
+
<li>
|
| 187 |
+
<span class="font-semibold">{key}</span>:
|
| 188 |
+
<span class="whitespace-pre-wrap">{formatValue(value)}</span>
|
| 189 |
+
</li>
|
| 190 |
+
{/if}
|
| 191 |
+
{/each}
|
| 192 |
+
</ul>
|
| 193 |
+
{:else if update.subtype === MessageToolUpdateType.Error}
|
| 194 |
+
<div class="mt-1 flex items-center gap-2 opacity-80">
|
| 195 |
+
<h3 class="text-sm">Error</h3>
|
| 196 |
+
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
|
| 197 |
+
</div>
|
| 198 |
+
<p class="text-sm">{update.message}</p>
|
| 199 |
+
{:else if isMessageToolResultUpdate(update) && update.result.status === ToolResultStatus.Success && update.result.display}
|
| 200 |
+
<div class="mt-1 flex items-center gap-2 opacity-80">
|
| 201 |
+
<h3 class="text-sm">Result</h3>
|
| 202 |
+
<div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div>
|
| 203 |
+
</div>
|
| 204 |
+
<ul class="py-1 text-sm">
|
| 205 |
+
{#each update.result.outputs as output}
|
| 206 |
+
{#each Object.entries(output) as [key, value]}
|
| 207 |
+
{#if value != null}
|
| 208 |
+
<li>
|
| 209 |
+
<span class="font-semibold">{key}</span>:
|
| 210 |
+
<span class="whitespace-pre-wrap">{formatValue(value)}</span>
|
| 211 |
+
</li>
|
| 212 |
+
{/if}
|
| 213 |
+
{/each}
|
| 214 |
+
{/each}
|
| 215 |
+
</ul>
|
| 216 |
+
{:else if isMessageToolResultUpdate(update) && update.result.status === ToolResultStatus.Error && update.result.display}
|
| 217 |
+
<div class="mt-1 flex items-center gap-2 opacity-80">
|
| 218 |
+
<h3 class="text-sm text-red-600 dark:text-red-400">Error</h3>
|
| 219 |
+
<div class="h-px flex-1 bg-gradient-to-r from-red-500/20"></div>
|
| 220 |
+
</div>
|
| 221 |
+
<p class="whitespace-pre-wrap text-sm text-red-600 dark:text-red-400">
|
| 222 |
+
{update.result.message}
|
| 223 |
+
</p>
|
| 224 |
+
{/if}
|
| 225 |
+
{/each}
|
| 226 |
+
</details>
|
| 227 |
+
{/if}
|
| 228 |
+
|
| 229 |
+
<style>
|
| 230 |
+
details summary::-webkit-details-marker {
|
| 231 |
+
display: none;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
@keyframes loading {
|
| 235 |
+
to {
|
| 236 |
+
/* move one full perimeter, normalized via pathLength=100 */
|
| 237 |
+
stroke-dashoffset: -100;
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.loading-path {
|
| 242 |
+
/* larger traveling gap for clearer motion */
|
| 243 |
+
stroke-dasharray: 80 20; /* 80% dash, 20% gap */
|
| 244 |
+
animation: loading 1.6s linear infinite;
|
| 245 |
+
}
|
| 246 |
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
interface Props {
|
| 3 |
+
classNames?: string;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
let { classNames = "" }: Props = $props();
|
| 7 |
+
</script>
|
| 8 |
+
|
| 9 |
+
<svg
|
| 10 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 11 |
+
class={classNames}
|
| 12 |
+
width="1em"
|
| 13 |
+
height="1em"
|
| 14 |
+
viewBox="0 0 24 24"
|
| 15 |
+
>
|
| 16 |
+
<g
|
| 17 |
+
fill="none"
|
| 18 |
+
stroke="currentColor"
|
| 19 |
+
stroke-linecap="round"
|
| 20 |
+
stroke-linejoin="round"
|
| 21 |
+
stroke-width="1.5"
|
| 22 |
+
>
|
| 23 |
+
<path
|
| 24 |
+
d="m3.5 11.75l8.172-8.171a2.828 2.828 0 1 1 4 4m0 0L9.5 13.75m6.172-6.171a2.828 2.828 0 0 1 4 4l-6.965 6.964a1 1 0 0 0 0 1.414L14 21.25"
|
| 25 |
+
/>
|
| 26 |
+
<path d="m17.5 9.75l-6.172 6.171a2.829 2.829 0 0 1-4-4L13.5 5.749" />
|
| 27 |
+
</g>
|
| 28 |
+
</svg>
|
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { KeyValuePair } from "$lib/types/Tool";
|
| 3 |
+
import {
|
| 4 |
+
validateMcpServerUrl,
|
| 5 |
+
validateHeader,
|
| 6 |
+
isSensitiveHeader,
|
| 7 |
+
} from "$lib/utils/mcpValidation";
|
| 8 |
+
import IconEye from "~icons/carbon/view";
|
| 9 |
+
import IconEyeOff from "~icons/carbon/view-off";
|
| 10 |
+
import IconTrash from "~icons/carbon/trash-can";
|
| 11 |
+
import IconAdd from "~icons/carbon/add";
|
| 12 |
+
import IconWarning from "~icons/carbon/warning";
|
| 13 |
+
|
| 14 |
+
interface Props {
|
| 15 |
+
onsubmit: (server: { name: string; url: string; headers?: KeyValuePair[] }) => void;
|
| 16 |
+
oncancel: () => void;
|
| 17 |
+
initialName?: string;
|
| 18 |
+
initialUrl?: string;
|
| 19 |
+
initialHeaders?: KeyValuePair[];
|
| 20 |
+
submitLabel?: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
let {
|
| 24 |
+
onsubmit,
|
| 25 |
+
oncancel,
|
| 26 |
+
initialName = "",
|
| 27 |
+
initialUrl = "",
|
| 28 |
+
initialHeaders = [],
|
| 29 |
+
submitLabel = "Add Server",
|
| 30 |
+
}: Props = $props();
|
| 31 |
+
|
| 32 |
+
let name = $state(initialName);
|
| 33 |
+
let url = $state(initialUrl);
|
| 34 |
+
let headers = $state<KeyValuePair[]>(initialHeaders.length > 0 ? [...initialHeaders] : []);
|
| 35 |
+
let showHeaderValues = $state<Record<number, boolean>>({});
|
| 36 |
+
let error = $state<string | null>(null);
|
| 37 |
+
|
| 38 |
+
function addHeader() {
|
| 39 |
+
headers = [...headers, { key: "", value: "" }];
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function removeHeader(index: number) {
|
| 43 |
+
headers = headers.filter((_, i) => i !== index);
|
| 44 |
+
delete showHeaderValues[index];
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function toggleHeaderVisibility(index: number) {
|
| 48 |
+
showHeaderValues = {
|
| 49 |
+
...showHeaderValues,
|
| 50 |
+
[index]: !showHeaderValues[index],
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function validate(): boolean {
|
| 55 |
+
if (!name.trim()) {
|
| 56 |
+
error = "Server name is required";
|
| 57 |
+
return false;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (!url.trim()) {
|
| 61 |
+
error = "Server URL is required";
|
| 62 |
+
return false;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const urlValidation = validateMcpServerUrl(url);
|
| 66 |
+
if (!urlValidation) {
|
| 67 |
+
error = "Invalid URL.";
|
| 68 |
+
return false;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// Validate headers
|
| 72 |
+
for (let i = 0; i < headers.length; i++) {
|
| 73 |
+
const header = headers[i];
|
| 74 |
+
if (header.key.trim() || header.value.trim()) {
|
| 75 |
+
const headerError = validateHeader(header.key, header.value);
|
| 76 |
+
if (headerError) {
|
| 77 |
+
error = `Header ${i + 1}: ${headerError}`;
|
| 78 |
+
return false;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
error = null;
|
| 84 |
+
return true;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function handleSubmit() {
|
| 88 |
+
if (!validate()) return;
|
| 89 |
+
|
| 90 |
+
// Filter out empty headers
|
| 91 |
+
const filteredHeaders = headers.filter((h) => h.key.trim() && h.value.trim());
|
| 92 |
+
|
| 93 |
+
onsubmit({
|
| 94 |
+
name: name.trim(),
|
| 95 |
+
url: url.trim(),
|
| 96 |
+
headers: filteredHeaders.length > 0 ? filteredHeaders : undefined,
|
| 97 |
+
});
|
| 98 |
+
}
|
| 99 |
+
</script>
|
| 100 |
+
|
| 101 |
+
<div class="space-y-4">
|
| 102 |
+
<!-- Server Name -->
|
| 103 |
+
<div>
|
| 104 |
+
<label
|
| 105 |
+
for="server-name"
|
| 106 |
+
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
| 107 |
+
>
|
| 108 |
+
Server Name <span class="text-red-500">*</span>
|
| 109 |
+
</label>
|
| 110 |
+
<input
|
| 111 |
+
id="server-name"
|
| 112 |
+
type="text"
|
| 113 |
+
bind:value={name}
|
| 114 |
+
placeholder="My MCP Server"
|
| 115 |
+
class="mt-1.5 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
| 116 |
+
/>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- Server URL -->
|
| 120 |
+
<div>
|
| 121 |
+
<label for="server-url" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 122 |
+
Server URL <span class="text-red-500">*</span>
|
| 123 |
+
</label>
|
| 124 |
+
<input
|
| 125 |
+
id="server-url"
|
| 126 |
+
type="url"
|
| 127 |
+
bind:value={url}
|
| 128 |
+
placeholder="https://example.com/mcp"
|
| 129 |
+
class="mt-1.5 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
| 130 |
+
/>
|
| 131 |
+
<!-- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
| 132 |
+
Only HTTPS is supported (e.g., https://localhost:5101).
|
| 133 |
+
</p> -->
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<!-- HTTP Headers -->
|
| 137 |
+
<details class="rounded-lg border border-gray-200 dark:border-gray-700">
|
| 138 |
+
<summary class="cursor-pointer px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 139 |
+
HTTP Headers (Optional)
|
| 140 |
+
</summary>
|
| 141 |
+
<div class="space-y-2 border-t border-gray-200 p-4 dark:border-gray-700">
|
| 142 |
+
{#if headers.length === 0}
|
| 143 |
+
<p class="text-sm text-gray-500 dark:text-gray-400">No headers configured</p>
|
| 144 |
+
{:else}
|
| 145 |
+
{#each headers as header, i}
|
| 146 |
+
<div class="flex gap-2">
|
| 147 |
+
<input
|
| 148 |
+
bind:value={header.key}
|
| 149 |
+
placeholder="Header name (e.g., Authorization)"
|
| 150 |
+
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
| 151 |
+
/>
|
| 152 |
+
<div class="relative flex-1">
|
| 153 |
+
<input
|
| 154 |
+
bind:value={header.value}
|
| 155 |
+
type={showHeaderValues[i] ? "text" : "password"}
|
| 156 |
+
placeholder="Value"
|
| 157 |
+
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
| 158 |
+
/>
|
| 159 |
+
{#if isSensitiveHeader(header.key)}
|
| 160 |
+
<button
|
| 161 |
+
type="button"
|
| 162 |
+
onclick={() => toggleHeaderVisibility(i)}
|
| 163 |
+
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
| 164 |
+
title={showHeaderValues[i] ? "Hide value" : "Show value"}
|
| 165 |
+
>
|
| 166 |
+
{#if showHeaderValues[i]}
|
| 167 |
+
<IconEyeOff class="size-4" />
|
| 168 |
+
{:else}
|
| 169 |
+
<IconEye class="size-4" />
|
| 170 |
+
{/if}
|
| 171 |
+
</button>
|
| 172 |
+
{/if}
|
| 173 |
+
</div>
|
| 174 |
+
<button
|
| 175 |
+
type="button"
|
| 176 |
+
onclick={() => removeHeader(i)}
|
| 177 |
+
class="rounded-lg bg-red-100 p-2 text-red-600 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
|
| 178 |
+
title="Remove header"
|
| 179 |
+
>
|
| 180 |
+
<IconTrash class="size-4" />
|
| 181 |
+
</button>
|
| 182 |
+
</div>
|
| 183 |
+
{/each}
|
| 184 |
+
{/if}
|
| 185 |
+
|
| 186 |
+
<button
|
| 187 |
+
type="button"
|
| 188 |
+
onclick={addHeader}
|
| 189 |
+
class="flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
| 190 |
+
>
|
| 191 |
+
<IconAdd class="size-4" />
|
| 192 |
+
Add Header
|
| 193 |
+
</button>
|
| 194 |
+
|
| 195 |
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
| 196 |
+
Common examples:<br />
|
| 197 |
+
• Bearer token:
|
| 198 |
+
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700"
|
| 199 |
+
>Authorization: Bearer YOUR_TOKEN</code
|
| 200 |
+
><br />
|
| 201 |
+
• API key:
|
| 202 |
+
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700">X-API-Key: YOUR_KEY</code>
|
| 203 |
+
</p>
|
| 204 |
+
</div>
|
| 205 |
+
</details>
|
| 206 |
+
|
| 207 |
+
<!-- Security warning about custom MCP servers -->
|
| 208 |
+
<div
|
| 209 |
+
class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-amber-900 dark:border-yellow-900/40 dark:bg-yellow-900/20 dark:text-yellow-100"
|
| 210 |
+
>
|
| 211 |
+
<div class="flex items-start gap-3">
|
| 212 |
+
<IconWarning class="mt-0.5 size-4 flex-none text-amber-600 dark:text-yellow-300" />
|
| 213 |
+
<div class="text-sm leading-5">
|
| 214 |
+
<p class="font-medium">Be careful with custom MCP servers.</p>
|
| 215 |
+
<p class="mt-1 text-[13px] text-amber-800 dark:text-yellow-100/90">
|
| 216 |
+
They receive your requests (including conversation context and any headers you add) and
|
| 217 |
+
can run powerful tools on your behalf. Only add servers you trust and review their source.
|
| 218 |
+
Never share confidental informations.
|
| 219 |
+
</p>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<!-- Error message -->
|
| 225 |
+
{#if error}
|
| 226 |
+
<div
|
| 227 |
+
class="rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20"
|
| 228 |
+
>
|
| 229 |
+
<p class="text-sm text-red-800 dark:text-red-200">{error}</p>
|
| 230 |
+
</div>
|
| 231 |
+
{/if}
|
| 232 |
+
|
| 233 |
+
<!-- Actions -->
|
| 234 |
+
<div class="flex justify-end gap-2">
|
| 235 |
+
<button
|
| 236 |
+
type="button"
|
| 237 |
+
onclick={oncancel}
|
| 238 |
+
class="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
| 239 |
+
>
|
| 240 |
+
Cancel
|
| 241 |
+
</button>
|
| 242 |
+
<button
|
| 243 |
+
type="button"
|
| 244 |
+
onclick={handleSubmit}
|
| 245 |
+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600"
|
| 246 |
+
>
|
| 247 |
+
{submitLabel}
|
| 248 |
+
</button>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 3 |
+
import Modal from "$lib/components/Modal.svelte";
|
| 4 |
+
import ServerCard from "./ServerCard.svelte";
|
| 5 |
+
import AddServerForm from "./AddServerForm.svelte";
|
| 6 |
+
import {
|
| 7 |
+
allMcpServers,
|
| 8 |
+
selectedServerIds,
|
| 9 |
+
enabledServersCount,
|
| 10 |
+
addCustomServer,
|
| 11 |
+
refreshMcpServers,
|
| 12 |
+
healthCheckServer,
|
| 13 |
+
} from "$lib/stores/mcpServers";
|
| 14 |
+
import type { KeyValuePair } from "$lib/types/Tool";
|
| 15 |
+
import IconAddLarge from "~icons/carbon/add-large";
|
| 16 |
+
import IconRefresh from "~icons/carbon/renew";
|
| 17 |
+
import IconTools from "~icons/carbon/tools";
|
| 18 |
+
import IconMCP from "$lib/components/icons/IconMCP.svelte";
|
| 19 |
+
|
| 20 |
+
const publicConfig = usePublicConfig();
|
| 21 |
+
|
| 22 |
+
interface Props {
|
| 23 |
+
onclose: () => void;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
let { onclose }: Props = $props();
|
| 27 |
+
|
| 28 |
+
type View = "list" | "add";
|
| 29 |
+
let currentView = $state<View>("list");
|
| 30 |
+
let isRefreshing = $state(false);
|
| 31 |
+
|
| 32 |
+
const baseServers = $derived($allMcpServers.filter((s) => s.type === "base"));
|
| 33 |
+
const customServers = $derived($allMcpServers.filter((s) => s.type === "custom"));
|
| 34 |
+
const enabledCount = $derived($enabledServersCount);
|
| 35 |
+
|
| 36 |
+
function handleAddServer(serverData: { name: string; url: string; headers?: KeyValuePair[] }) {
|
| 37 |
+
addCustomServer(serverData);
|
| 38 |
+
currentView = "list";
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function handleCancel() {
|
| 42 |
+
currentView = "list";
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
async function handleRefresh() {
|
| 46 |
+
if (isRefreshing) return;
|
| 47 |
+
isRefreshing = true;
|
| 48 |
+
try {
|
| 49 |
+
await refreshMcpServers();
|
| 50 |
+
// After refreshing the list, re-run health checks for all known servers
|
| 51 |
+
const servers = $allMcpServers;
|
| 52 |
+
await Promise.allSettled(servers.map((s) => healthCheckServer(s)));
|
| 53 |
+
} finally {
|
| 54 |
+
isRefreshing = false;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
</script>
|
| 58 |
+
|
| 59 |
+
<Modal width={currentView === "list" ? "w-[800px]" : "w-[600px]"} {onclose} closeButton>
|
| 60 |
+
<div class="p-6">
|
| 61 |
+
<!-- Header -->
|
| 62 |
+
<div class="mb-6">
|
| 63 |
+
<h2 class="mb-1 text-xl font-semibold text-gray-900 dark:text-gray-200">
|
| 64 |
+
{#if currentView === "list"}
|
| 65 |
+
MCP Servers
|
| 66 |
+
{:else}
|
| 67 |
+
Add MCP server
|
| 68 |
+
{/if}
|
| 69 |
+
</h2>
|
| 70 |
+
<p class="text-sm text-gray-600 dark:text-gray-400">
|
| 71 |
+
{#if currentView === "list"}
|
| 72 |
+
Manage MCP servers to extend {publicConfig.PUBLIC_APP_NAME} with external tools.
|
| 73 |
+
{:else}
|
| 74 |
+
Add a custom MCP server to {publicConfig.PUBLIC_APP_NAME}.
|
| 75 |
+
{/if}
|
| 76 |
+
</p>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<!-- Content -->
|
| 80 |
+
{#if currentView === "list"}
|
| 81 |
+
<div
|
| 82 |
+
class="mb-6 flex justify-between rounded-lg p-4 max-sm:flex-col max-sm:gap-4 sm:items-center {!enabledCount
|
| 83 |
+
? 'bg-gray-100 dark:bg-white/5'
|
| 84 |
+
: 'bg-blue-50 dark:bg-blue-900/10'}"
|
| 85 |
+
>
|
| 86 |
+
<div class="flex items-center gap-3">
|
| 87 |
+
<div
|
| 88 |
+
class="flex size-10 items-center justify-center rounded-xl bg-blue-500/10"
|
| 89 |
+
class:grayscale={!enabledCount}
|
| 90 |
+
>
|
| 91 |
+
<IconMCP classNames="size-8 text-blue-600 dark:text-blue-500" />
|
| 92 |
+
</div>
|
| 93 |
+
<div>
|
| 94 |
+
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
| 95 |
+
{$allMcpServers.length}
|
| 96 |
+
{$allMcpServers.length === 1 ? "server" : "servers"} configured
|
| 97 |
+
</p>
|
| 98 |
+
<p class="text-xs text-gray-600 dark:text-gray-400">
|
| 99 |
+
{enabledCount} enabled
|
| 100 |
+
</p>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<div class="flex gap-2">
|
| 105 |
+
<button
|
| 106 |
+
onclick={handleRefresh}
|
| 107 |
+
disabled={isRefreshing}
|
| 108 |
+
class="btn gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
| 109 |
+
>
|
| 110 |
+
<IconRefresh class="size-4 {isRefreshing ? 'animate-spin' : ''}" />
|
| 111 |
+
{isRefreshing ? "Refreshing…" : "Refresh"}
|
| 112 |
+
</button>
|
| 113 |
+
<button
|
| 114 |
+
onclick={() => (currentView = "add")}
|
| 115 |
+
class="btn flex items-center gap-0.5 rounded-lg bg-blue-600 py-1.5 pl-2 pr-3 text-sm font-medium text-white hover:bg-blue-600"
|
| 116 |
+
>
|
| 117 |
+
<IconAddLarge class="size-4" />
|
| 118 |
+
Add Server
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="space-y-5">
|
| 123 |
+
<!-- Base Servers -->
|
| 124 |
+
{#if baseServers.length > 0}
|
| 125 |
+
<div>
|
| 126 |
+
<h3 class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 127 |
+
Base Servers ({baseServers.length})
|
| 128 |
+
</h3>
|
| 129 |
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
| 130 |
+
{#each baseServers as server (server.id)}
|
| 131 |
+
<ServerCard {server} isSelected={$selectedServerIds.has(server.id)} />
|
| 132 |
+
{/each}
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
{/if}
|
| 136 |
+
|
| 137 |
+
<!-- Custom Servers -->
|
| 138 |
+
<div>
|
| 139 |
+
<h3 class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 140 |
+
Custom Servers ({customServers.length})
|
| 141 |
+
</h3>
|
| 142 |
+
{#if customServers.length === 0}
|
| 143 |
+
<div
|
| 144 |
+
class="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 p-8 dark:border-gray-700"
|
| 145 |
+
>
|
| 146 |
+
<IconTools class="mb-3 size-12 text-gray-400" />
|
| 147 |
+
<p class="mb-1 text-sm font-medium text-gray-900 dark:text-gray-100">
|
| 148 |
+
No custom servers yet
|
| 149 |
+
</p>
|
| 150 |
+
<p class="mb-4 text-xs text-gray-600 dark:text-gray-400">
|
| 151 |
+
Add your own MCP servers with custom tools
|
| 152 |
+
</p>
|
| 153 |
+
<button
|
| 154 |
+
onclick={() => (currentView = "add")}
|
| 155 |
+
class="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600"
|
| 156 |
+
>
|
| 157 |
+
<IconAddLarge class="size-4" />
|
| 158 |
+
Add Your First Server
|
| 159 |
+
</button>
|
| 160 |
+
</div>
|
| 161 |
+
{:else}
|
| 162 |
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
| 163 |
+
{#each customServers as server (server.id)}
|
| 164 |
+
<ServerCard {server} isSelected={$selectedServerIds.has(server.id)} />
|
| 165 |
+
{/each}
|
| 166 |
+
</div>
|
| 167 |
+
{/if}
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<!-- Help Text -->
|
| 171 |
+
<div class="rounded-lg bg-gray-50 p-4 dark:bg-gray-700">
|
| 172 |
+
<h4 class="mb-2 text-sm font-medium text-gray-900 dark:text-gray-100">💡 Quick Tips</h4>
|
| 173 |
+
<ul class="space-y-1 text-xs text-gray-600 dark:text-gray-400">
|
| 174 |
+
<li>• Only connect to servers you trust</li>
|
| 175 |
+
<li>• Enable servers to make their tools available in chat</li>
|
| 176 |
+
<li>• Use the Health Check button to verify server connectivity</li>
|
| 177 |
+
<li>• You can add HTTP headers for authentication when required</li>
|
| 178 |
+
</ul>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
{:else if currentView === "add"}
|
| 182 |
+
<AddServerForm onsubmit={handleAddServer} oncancel={handleCancel} />
|
| 183 |
+
{/if}
|
| 184 |
+
</div>
|
| 185 |
+
</Modal>
|
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { MCPServer } from "$lib/types/Tool";
|
| 3 |
+
import { toggleServer, healthCheckServer, deleteCustomServer } from "$lib/stores/mcpServers";
|
| 4 |
+
import IconCheckmark from "~icons/carbon/checkmark-filled";
|
| 5 |
+
import IconWarning from "~icons/carbon/warning-filled";
|
| 6 |
+
import IconPending from "~icons/carbon/pending-filled";
|
| 7 |
+
import IconRefresh from "~icons/carbon/renew";
|
| 8 |
+
import IconTrash from "~icons/carbon/trash-can";
|
| 9 |
+
import IconTools from "~icons/carbon/tools";
|
| 10 |
+
import IconSettings from "~icons/carbon/settings";
|
| 11 |
+
import Switch from "$lib/components/Switch.svelte";
|
| 12 |
+
import { getMcpServerFaviconUrl } from "$lib/utils/favicon";
|
| 13 |
+
|
| 14 |
+
interface Props {
|
| 15 |
+
server: MCPServer;
|
| 16 |
+
isSelected: boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
let { server, isSelected }: Props = $props();
|
| 20 |
+
|
| 21 |
+
let isLoadingHealth = $state(false);
|
| 22 |
+
|
| 23 |
+
// Show a quick-access link ONLY for the exact HF MCP login endpoint
|
| 24 |
+
import { isStrictHfMcpLogin as isStrictHfMcpLoginUrl } from "$lib/utils/hf";
|
| 25 |
+
const isHfMcp = $derived.by(() => isStrictHfMcpLoginUrl(server.url));
|
| 26 |
+
|
| 27 |
+
const statusInfo = $derived.by(() => {
|
| 28 |
+
switch (server.status) {
|
| 29 |
+
case "connected":
|
| 30 |
+
return {
|
| 31 |
+
label: "Connected",
|
| 32 |
+
color: "text-green-600 dark:text-green-400",
|
| 33 |
+
bgColor: "bg-green-100 dark:bg-green-900/20",
|
| 34 |
+
icon: IconCheckmark,
|
| 35 |
+
};
|
| 36 |
+
case "connecting":
|
| 37 |
+
return {
|
| 38 |
+
label: "Connecting...",
|
| 39 |
+
color: "text-blue-600 dark:text-blue-400",
|
| 40 |
+
bgColor: "bg-blue-100 dark:bg-blue-900/20",
|
| 41 |
+
icon: IconPending,
|
| 42 |
+
};
|
| 43 |
+
case "error":
|
| 44 |
+
return {
|
| 45 |
+
label: "Error",
|
| 46 |
+
color: "text-red-600 dark:text-red-400",
|
| 47 |
+
bgColor: "bg-red-100 dark:bg-red-900/20",
|
| 48 |
+
icon: IconWarning,
|
| 49 |
+
};
|
| 50 |
+
case "disconnected":
|
| 51 |
+
default:
|
| 52 |
+
return {
|
| 53 |
+
label: "Unknown",
|
| 54 |
+
color: "text-gray-600 dark:text-gray-400",
|
| 55 |
+
bgColor: "bg-gray-100 dark:bg-gray-700",
|
| 56 |
+
icon: IconPending,
|
| 57 |
+
};
|
| 58 |
+
}
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// Switch setter handles enable/disable (simple, idiomatic)
|
| 62 |
+
function setEnabled(v: boolean) {
|
| 63 |
+
if (v === isSelected) return;
|
| 64 |
+
toggleServer(server.id);
|
| 65 |
+
if (v && server.status !== "connected") handleHealthCheck();
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
async function handleHealthCheck() {
|
| 69 |
+
isLoadingHealth = true;
|
| 70 |
+
try {
|
| 71 |
+
await healthCheckServer(server);
|
| 72 |
+
} finally {
|
| 73 |
+
isLoadingHealth = false;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function handleDelete() {
|
| 78 |
+
deleteCustomServer(server.id);
|
| 79 |
+
}
|
| 80 |
+
</script>
|
| 81 |
+
|
| 82 |
+
<div
|
| 83 |
+
class="rounded-lg border bg-gradient-to-br transition-colors {isSelected
|
| 84 |
+
? 'border-blue-600/20 bg-blue-50 from-blue-500/5 to-transparent dark:border-blue-700/60 dark:bg-blue-900/10 dark:from-blue-900/20'
|
| 85 |
+
: 'border-gray-200 bg-white from-black/5 dark:border-gray-700 dark:bg-gray-800 dark:from-white/5'}"
|
| 86 |
+
>
|
| 87 |
+
<div class="px-4 py-3.5">
|
| 88 |
+
<!-- Header -->
|
| 89 |
+
<div class="mb-3 flex items-start justify-between gap-3">
|
| 90 |
+
<div class="min-w-0 flex-1">
|
| 91 |
+
<div class="mb-0.5 flex items-center gap-2">
|
| 92 |
+
<img
|
| 93 |
+
src={getMcpServerFaviconUrl(server.url)}
|
| 94 |
+
alt=""
|
| 95 |
+
class="size-4 flex-shrink-0 rounded"
|
| 96 |
+
/>
|
| 97 |
+
<h3 class="truncate font-semibold text-gray-900 dark:text-gray-100">
|
| 98 |
+
{server.name}
|
| 99 |
+
</h3>
|
| 100 |
+
</div>
|
| 101 |
+
<p class="truncate text-sm text-gray-600 dark:text-gray-400">
|
| 102 |
+
{server.url}
|
| 103 |
+
</p>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<!-- Enable Switch (function binding per Svelte 5 docs) -->
|
| 107 |
+
<Switch name={`enable-${server.id}`} bind:checked={() => isSelected, setEnabled} />
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<!-- Status -->
|
| 111 |
+
{#if server.status}
|
| 112 |
+
<div class="mb-2 flex items-center gap-2">
|
| 113 |
+
<span
|
| 114 |
+
class="inline-flex items-center gap-1 rounded-full {statusInfo.bgColor} py-0.5 pl-1.5 pr-2 text-xs font-medium {statusInfo.color}"
|
| 115 |
+
>
|
| 116 |
+
{#if server.status === "connected"}
|
| 117 |
+
<IconCheckmark class="size-3" />
|
| 118 |
+
{:else if server.status === "connecting"}
|
| 119 |
+
<IconPending class="size-3" />
|
| 120 |
+
{:else if server.status === "error"}
|
| 121 |
+
<IconWarning class="size-3" />
|
| 122 |
+
{:else}
|
| 123 |
+
<IconPending class="size-3" />
|
| 124 |
+
{/if}
|
| 125 |
+
{statusInfo.label}
|
| 126 |
+
</span>
|
| 127 |
+
|
| 128 |
+
{#if server.tools && server.tools.length > 0}
|
| 129 |
+
<span class="inline-flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
|
| 130 |
+
<IconTools class="size-3" />
|
| 131 |
+
{server.tools.length}
|
| 132 |
+
{server.tools.length === 1 ? "tool" : "tools"}
|
| 133 |
+
</span>
|
| 134 |
+
{/if}
|
| 135 |
+
</div>
|
| 136 |
+
{/if}
|
| 137 |
+
|
| 138 |
+
<!-- Error Message -->
|
| 139 |
+
{#if server.errorMessage}
|
| 140 |
+
<div class="mb-2 flex items-center gap-2">
|
| 141 |
+
<div
|
| 142 |
+
class="rounded bg-red-50 px-2 py-1 text-xs text-red-800 dark:bg-red-900/20 dark:text-red-200"
|
| 143 |
+
>
|
| 144 |
+
{server.errorMessage}
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
{/if}
|
| 148 |
+
|
| 149 |
+
<!-- Actions -->
|
| 150 |
+
<div class="flex flex-wrap gap-1">
|
| 151 |
+
<button
|
| 152 |
+
onclick={handleHealthCheck}
|
| 153 |
+
disabled={isLoadingHealth}
|
| 154 |
+
class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-[.29rem] text-xs font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
| 155 |
+
>
|
| 156 |
+
<IconRefresh class="size-3 {isLoadingHealth ? 'animate-spin' : ''}" />
|
| 157 |
+
Health Check
|
| 158 |
+
</button>
|
| 159 |
+
|
| 160 |
+
{#if isHfMcp}
|
| 161 |
+
<a
|
| 162 |
+
href="https://huggingface.co/settings/mcp"
|
| 163 |
+
target="_blank"
|
| 164 |
+
rel="noopener noreferrer"
|
| 165 |
+
class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-[.29rem] text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
| 166 |
+
aria-label="Open Hugging Face MCP settings"
|
| 167 |
+
>
|
| 168 |
+
<IconSettings class="size-3" />
|
| 169 |
+
Settings
|
| 170 |
+
</a>
|
| 171 |
+
{/if}
|
| 172 |
+
|
| 173 |
+
{#if server.type === "custom"}
|
| 174 |
+
<button
|
| 175 |
+
onclick={handleDelete}
|
| 176 |
+
class="flex items-center gap-1.5 rounded-lg border border-red-500/15 bg-red-50 px-2.5 py-[.29rem] text-xs font-medium text-red-600 hover:bg-red-100 dark:border-red-500/25 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
|
| 177 |
+
>
|
| 178 |
+
<IconTrash class="size-3" />
|
| 179 |
+
Delete
|
| 180 |
+
</button>
|
| 181 |
+
{/if}
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<!-- Tools List (Expandable) -->
|
| 185 |
+
{#if server.tools && server.tools.length > 0}
|
| 186 |
+
<details class="mt-3">
|
| 187 |
+
<summary class="cursor-pointer text-xs font-medium text-gray-700 dark:text-gray-300">
|
| 188 |
+
Available Tools ({server.tools.length})
|
| 189 |
+
</summary>
|
| 190 |
+
<ul class="mt-2 space-y-1 text-xs">
|
| 191 |
+
{#each server.tools as tool}
|
| 192 |
+
<li class="text-gray-600 dark:text-gray-400">
|
| 193 |
+
<span class="font-medium text-gray-900 dark:text-gray-100">{tool.name}</span>
|
| 194 |
+
{#if tool.description}
|
| 195 |
+
<span class="text-gray-500 dark:text-gray-500">- {tool.description}</span>
|
| 196 |
+
{/if}
|
| 197 |
+
</li>
|
| 198 |
+
{/each}
|
| 199 |
+
</ul>
|
| 200 |
+
</details>
|
| 201 |
+
{/if}
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
|
@@ -21,6 +21,7 @@ export type GETModelsResponse = Array<{
|
|
| 21 |
preprompt?: string;
|
| 22 |
multimodal: boolean;
|
| 23 |
multimodalAcceptedMimetypes?: string[];
|
|
|
|
| 24 |
unlisted: boolean;
|
| 25 |
hasInferenceAPI: boolean;
|
| 26 |
// Mark router entry for UI decoration — always present
|
|
@@ -59,6 +60,7 @@ export const modelGroup = new Elysia().group("/models", (app) =>
|
|
| 59 |
preprompt: model.preprompt,
|
| 60 |
multimodal: model.multimodal,
|
| 61 |
multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
|
|
|
|
| 62 |
unlisted: model.unlisted,
|
| 63 |
hasInferenceAPI: model.hasInferenceAPI,
|
| 64 |
isRouter: model.isRouter,
|
|
|
|
| 21 |
preprompt?: string;
|
| 22 |
multimodal: boolean;
|
| 23 |
multimodalAcceptedMimetypes?: string[];
|
| 24 |
+
supportsTools?: boolean;
|
| 25 |
unlisted: boolean;
|
| 26 |
hasInferenceAPI: boolean;
|
| 27 |
// Mark router entry for UI decoration — always present
|
|
|
|
| 60 |
preprompt: model.preprompt,
|
| 61 |
multimodal: model.multimodal,
|
| 62 |
multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
|
| 63 |
+
supportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,
|
| 64 |
unlisted: model.unlisted,
|
| 65 |
hasInferenceAPI: model.hasInferenceAPI,
|
| 66 |
isRouter: model.isRouter,
|
|
@@ -71,6 +71,7 @@ export const userGroup = new Elysia()
|
|
| 71 |
|
| 72 |
customPrompts: settings?.customPrompts ?? {},
|
| 73 |
multimodalOverrides: settings?.multimodalOverrides ?? {},
|
|
|
|
| 74 |
};
|
| 75 |
})
|
| 76 |
.post("/settings", async ({ locals, request }) => {
|
|
@@ -85,14 +86,13 @@ export const userGroup = new Elysia()
|
|
| 85 |
activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
|
| 86 |
customPrompts: z.record(z.string()).default({}),
|
| 87 |
multimodalOverrides: z.record(z.boolean()).default({}),
|
|
|
|
| 88 |
disableStream: z.boolean().default(false),
|
| 89 |
directPaste: z.boolean().default(false),
|
| 90 |
hidePromptExamples: z.record(z.boolean()).default({}),
|
| 91 |
})
|
| 92 |
.parse(body) satisfies SettingsEditable;
|
| 93 |
|
| 94 |
-
// Tools removed: ignore tools updates
|
| 95 |
-
|
| 96 |
await collections.settings.updateOne(
|
| 97 |
authCondition(locals),
|
| 98 |
{
|
|
|
|
| 71 |
|
| 72 |
customPrompts: settings?.customPrompts ?? {},
|
| 73 |
multimodalOverrides: settings?.multimodalOverrides ?? {},
|
| 74 |
+
toolsOverrides: settings?.toolsOverrides ?? {},
|
| 75 |
};
|
| 76 |
})
|
| 77 |
.post("/settings", async ({ locals, request }) => {
|
|
|
|
| 86 |
activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
|
| 87 |
customPrompts: z.record(z.string()).default({}),
|
| 88 |
multimodalOverrides: z.record(z.boolean()).default({}),
|
| 89 |
+
toolsOverrides: z.record(z.boolean()).default({}),
|
| 90 |
disableStream: z.boolean().default(false),
|
| 91 |
directPaste: z.boolean().default(false),
|
| 92 |
hidePromptExamples: z.record(z.boolean()).default({}),
|
| 93 |
})
|
| 94 |
.parse(body) satisfies SettingsEditable;
|
| 95 |
|
|
|
|
|
|
|
| 96 |
await collections.settings.updateOne(
|
| 97 |
authCondition(locals),
|
| 98 |
{
|
|
@@ -156,7 +156,9 @@ type ExtraConfigKeys =
|
|
| 156 |
| "OLD_MODELS"
|
| 157 |
| "ENABLE_ASSISTANTS"
|
| 158 |
| "METRICS_ENABLED"
|
| 159 |
-
| "METRICS_PORT"
|
|
|
|
|
|
|
| 160 |
|
| 161 |
type ConfigProxy = ConfigManager & { [K in ConfigKey | ExtraConfigKeys]: string };
|
| 162 |
|
|
|
|
| 156 |
| "OLD_MODELS"
|
| 157 |
| "ENABLE_ASSISTANTS"
|
| 158 |
| "METRICS_ENABLED"
|
| 159 |
+
| "METRICS_PORT"
|
| 160 |
+
| "MCP_SERVERS"
|
| 161 |
+
| "MCP_FORWARD_HF_USER_TOKEN";
|
| 162 |
|
| 163 |
type ConfigProxy = ConfigManager & { [K in ConfigKey | ExtraConfigKeys]: string };
|
| 164 |
|
|
@@ -171,12 +171,10 @@ export async function endpointOai(
|
|
| 171 |
await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal);
|
| 172 |
|
| 173 |
// Normalize preprompt and handle empty values
|
| 174 |
-
const normalizedPreprompt =
|
| 175 |
-
typeof preprompt === "string" ? preprompt.trim() : "";
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system";
|
| 180 |
|
| 181 |
if (hasSystemMessage) {
|
| 182 |
// Prepend normalized preprompt to existing system content when non-empty
|
|
@@ -188,15 +186,12 @@ export async function endpointOai(
|
|
| 188 |
messagesOpenAI[0].content =
|
| 189 |
normalizedPreprompt + (userSystemPrompt ? "\n\n" + userSystemPrompt : "");
|
| 190 |
}
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
{ role: "system", content: normalizedPreprompt },
|
| 196 |
-
...messagesOpenAI,
|
| 197 |
-
];
|
| 198 |
-
}
|
| 199 |
}
|
|
|
|
| 200 |
|
| 201 |
// Combine model defaults with request-specific parameters
|
| 202 |
const parameters = { ...model.parameters, ...generateSettings };
|
|
|
|
| 171 |
await prepareMessages(messages, imageProcessor, isMultimodal ?? model.multimodal);
|
| 172 |
|
| 173 |
// Normalize preprompt and handle empty values
|
| 174 |
+
const normalizedPreprompt = typeof preprompt === "string" ? preprompt.trim() : "";
|
|
|
|
| 175 |
|
| 176 |
+
// Check if a system message already exists as the first message
|
| 177 |
+
const hasSystemMessage = messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system";
|
|
|
|
| 178 |
|
| 179 |
if (hasSystemMessage) {
|
| 180 |
// Prepend normalized preprompt to existing system content when non-empty
|
|
|
|
| 186 |
messagesOpenAI[0].content =
|
| 187 |
normalizedPreprompt + (userSystemPrompt ? "\n\n" + userSystemPrompt : "");
|
| 188 |
}
|
| 189 |
+
} else {
|
| 190 |
+
// Insert a system message only if the preprompt is non-empty
|
| 191 |
+
if (normalizedPreprompt) {
|
| 192 |
+
messagesOpenAI = [{ role: "system", content: normalizedPreprompt }, ...messagesOpenAI];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
}
|
| 194 |
+
}
|
| 195 |
|
| 196 |
// Combine model defaults with request-specific parameters
|
| 197 |
const parameters = { ...model.parameters, ...generateSettings };
|
|
@@ -4,13 +4,13 @@ import { downloadFile } from "../files/downloadFile";
|
|
| 4 |
import type { ObjectId } from "mongodb";
|
| 5 |
|
| 6 |
export async function preprocessMessages(
|
| 7 |
-
|
| 8 |
-
|
| 9 |
): Promise<EndpointMessage[]> {
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
}
|
| 15 |
|
| 16 |
async function downloadFiles(messages: Message[], convId: ObjectId): Promise<EndpointMessage[]> {
|
|
@@ -24,8 +24,8 @@ async function downloadFiles(messages: Message[], convId: ObjectId): Promise<End
|
|
| 24 |
}
|
| 25 |
|
| 26 |
async function injectClipboardFiles(messages: EndpointMessage[]) {
|
| 27 |
-
|
| 28 |
-
|
| 29 |
const plaintextFiles = message.files
|
| 30 |
?.filter((file) => file.mime === "application/vnd.chatui.clipboard")
|
| 31 |
.map((file) => Buffer.from(file.value, "base64").toString("utf-8"));
|
|
@@ -37,8 +37,8 @@ async function injectClipboardFiles(messages: EndpointMessage[]) {
|
|
| 37 |
content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`,
|
| 38 |
files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"),
|
| 39 |
};
|
| 40 |
-
|
| 41 |
-
|
| 42 |
}
|
| 43 |
|
| 44 |
/**
|
|
@@ -46,17 +46,16 @@ async function injectClipboardFiles(messages: EndpointMessage[]) {
|
|
| 46 |
* This prevents sending an empty system prompt to any provider.
|
| 47 |
*/
|
| 48 |
function stripEmptyInitialSystemMessage(messages: EndpointMessage[]): EndpointMessage[] {
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
typeof content === "string" ? content.trim().length === 0 : false;
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
|
| 61 |
-
|
| 62 |
}
|
|
|
|
| 4 |
import type { ObjectId } from "mongodb";
|
| 5 |
|
| 6 |
export async function preprocessMessages(
|
| 7 |
+
messages: Message[],
|
| 8 |
+
convId: ObjectId
|
| 9 |
): Promise<EndpointMessage[]> {
|
| 10 |
+
return Promise.resolve(messages)
|
| 11 |
+
.then((msgs) => downloadFiles(msgs, convId))
|
| 12 |
+
.then((msgs) => injectClipboardFiles(msgs))
|
| 13 |
+
.then(stripEmptyInitialSystemMessage);
|
| 14 |
}
|
| 15 |
|
| 16 |
async function downloadFiles(messages: Message[], convId: ObjectId): Promise<EndpointMessage[]> {
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
async function injectClipboardFiles(messages: EndpointMessage[]) {
|
| 27 |
+
return Promise.all(
|
| 28 |
+
messages.map((message) => {
|
| 29 |
const plaintextFiles = message.files
|
| 30 |
?.filter((file) => file.mime === "application/vnd.chatui.clipboard")
|
| 31 |
.map((file) => Buffer.from(file.value, "base64").toString("utf-8"));
|
|
|
|
| 37 |
content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`,
|
| 38 |
files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"),
|
| 39 |
};
|
| 40 |
+
})
|
| 41 |
+
);
|
| 42 |
}
|
| 43 |
|
| 44 |
/**
|
|
|
|
| 46 |
* This prevents sending an empty system prompt to any provider.
|
| 47 |
*/
|
| 48 |
function stripEmptyInitialSystemMessage(messages: EndpointMessage[]): EndpointMessage[] {
|
| 49 |
+
if (!messages?.length) return messages;
|
| 50 |
+
const first = messages[0];
|
| 51 |
+
if (first?.from !== "system") return messages;
|
| 52 |
|
| 53 |
+
const content = first?.content as unknown;
|
| 54 |
+
const isEmpty = typeof content === "string" ? content.trim().length === 0 : false;
|
|
|
|
| 55 |
|
| 56 |
+
if (isEmpty) {
|
| 57 |
+
return messages.slice(1);
|
| 58 |
+
}
|
| 59 |
|
| 60 |
+
return messages;
|
| 61 |
}
|
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
| 2 |
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
| 3 |
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
| 4 |
+
import type { McpServerConfig } from "./httpClient";
|
| 5 |
+
|
| 6 |
+
const pool = new Map<string, Client>();
|
| 7 |
+
|
| 8 |
+
function keyOf(server: McpServerConfig) {
|
| 9 |
+
const headers = Object.entries(server.headers ?? {})
|
| 10 |
+
.sort(([a], [b]) => a.localeCompare(b))
|
| 11 |
+
.map(([k, v]) => `${k}:${v}`)
|
| 12 |
+
.join("|\u0000|");
|
| 13 |
+
return `${server.url}|${headers}`;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export async function getClient(server: McpServerConfig, signal?: AbortSignal): Promise<Client> {
|
| 17 |
+
const key = keyOf(server);
|
| 18 |
+
const existing = pool.get(key);
|
| 19 |
+
if (existing) return existing;
|
| 20 |
+
|
| 21 |
+
const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
|
| 22 |
+
const url = new URL(server.url);
|
| 23 |
+
const requestInit: RequestInit = { headers: server.headers, signal };
|
| 24 |
+
try {
|
| 25 |
+
try {
|
| 26 |
+
await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
|
| 27 |
+
} catch {
|
| 28 |
+
await client.connect(new SSEClientTransport(url, { requestInit }));
|
| 29 |
+
}
|
| 30 |
+
} catch (err) {
|
| 31 |
+
try {
|
| 32 |
+
await client.close?.();
|
| 33 |
+
} catch {}
|
| 34 |
+
throw err;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
pool.set(key, client);
|
| 38 |
+
return client;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export async function drainPool() {
|
| 42 |
+
for (const [key, client] of pool) {
|
| 43 |
+
try {
|
| 44 |
+
await client.close?.();
|
| 45 |
+
} catch {}
|
| 46 |
+
pool.delete(key);
|
| 47 |
+
}
|
| 48 |
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Minimal shared helpers for HF MCP token forwarding
|
| 2 |
+
|
| 3 |
+
export const hasAuthHeader = (h?: Record<string, string>) =>
|
| 4 |
+
!!h && Object.keys(h).some((k) => k.toLowerCase() === "authorization");
|
| 5 |
+
|
| 6 |
+
export const isStrictHfMcpLogin = (urlString: string) => {
|
| 7 |
+
try {
|
| 8 |
+
const u = new URL(urlString);
|
| 9 |
+
return (
|
| 10 |
+
u.protocol === "https:" &&
|
| 11 |
+
u.hostname === "huggingface.co" &&
|
| 12 |
+
u.pathname === "/mcp" &&
|
| 13 |
+
u.search === "?login"
|
| 14 |
+
);
|
| 15 |
+
} catch {
|
| 16 |
+
return false;
|
| 17 |
+
}
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export const hasNonEmptyToken = (tok: unknown): tok is string =>
|
| 21 |
+
typeof tok === "string" && tok.trim().length > 0;
|
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
| 2 |
+
import { getClient } from "./clientPool";
|
| 3 |
+
|
| 4 |
+
export interface McpServerConfig {
|
| 5 |
+
name: string;
|
| 6 |
+
url: string;
|
| 7 |
+
headers?: Record<string, string>;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
| 11 |
+
|
| 12 |
+
export type McpToolTextResponse = {
|
| 13 |
+
text: string;
|
| 14 |
+
/** If the server returned structuredContent, include it raw */
|
| 15 |
+
structured?: unknown;
|
| 16 |
+
/** Raw content blocks returned by the server, if any */
|
| 17 |
+
content?: unknown[];
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export async function callMcpTool(
|
| 21 |
+
server: McpServerConfig,
|
| 22 |
+
tool: string,
|
| 23 |
+
args: unknown = {},
|
| 24 |
+
{
|
| 25 |
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
| 26 |
+
signal,
|
| 27 |
+
client,
|
| 28 |
+
}: { timeoutMs?: number; signal?: AbortSignal; client?: Client } = {}
|
| 29 |
+
): Promise<McpToolTextResponse> {
|
| 30 |
+
const normalizedArgs =
|
| 31 |
+
typeof args === "object" && args !== null && !Array.isArray(args)
|
| 32 |
+
? (args as Record<string, unknown>)
|
| 33 |
+
: undefined;
|
| 34 |
+
|
| 35 |
+
// Get a (possibly pooled) client. The client itself was connected with a signal
|
| 36 |
+
// that already composes outer cancellation. We still enforce a per-call timeout here.
|
| 37 |
+
const activeClient = client ?? (await getClient(server, signal));
|
| 38 |
+
|
| 39 |
+
// Prefer the SDK's built-in request controls (timeout, signal)
|
| 40 |
+
const response = await activeClient.callTool(
|
| 41 |
+
{ name: tool, arguments: normalizedArgs },
|
| 42 |
+
undefined,
|
| 43 |
+
{ signal, timeout: timeoutMs }
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
const parts = Array.isArray(response?.content) ? (response.content as Array<unknown>) : [];
|
| 47 |
+
const textParts = parts
|
| 48 |
+
.filter((part): part is { type: "text"; text: string } => {
|
| 49 |
+
if (typeof part !== "object" || part === null) return false;
|
| 50 |
+
const obj = part as Record<string, unknown>;
|
| 51 |
+
return obj["type"] === "text" && typeof obj["text"] === "string";
|
| 52 |
+
})
|
| 53 |
+
.map((p) => p.text);
|
| 54 |
+
|
| 55 |
+
const text = textParts.join("\n");
|
| 56 |
+
const structured = (response as unknown as { structuredContent?: unknown })?.structuredContent;
|
| 57 |
+
const contentBlocks = Array.isArray(response?.content)
|
| 58 |
+
? (response.content as unknown[])
|
| 59 |
+
: undefined;
|
| 60 |
+
return { text, structured, content: contentBlocks };
|
| 61 |
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
+
import { logger } from "$lib/server/logger";
|
| 3 |
+
import type { McpServerConfig } from "./httpClient";
|
| 4 |
+
import { resetMcpToolsCache } from "./tools";
|
| 5 |
+
|
| 6 |
+
let cachedRaw: string | null = null;
|
| 7 |
+
let cachedServers: McpServerConfig[] = [];
|
| 8 |
+
|
| 9 |
+
function parseServers(raw: string): McpServerConfig[] {
|
| 10 |
+
if (!raw) return [];
|
| 11 |
+
|
| 12 |
+
try {
|
| 13 |
+
const parsed = JSON.parse(raw);
|
| 14 |
+
if (!Array.isArray(parsed)) return [];
|
| 15 |
+
|
| 16 |
+
return parsed
|
| 17 |
+
.map((entry) => {
|
| 18 |
+
if (!entry || typeof entry !== "object") return undefined;
|
| 19 |
+
const name = (entry as Record<string, unknown>).name;
|
| 20 |
+
const url = (entry as Record<string, unknown>).url;
|
| 21 |
+
if (typeof name !== "string" || !name.trim()) return undefined;
|
| 22 |
+
if (typeof url !== "string" || !url.trim()) return undefined;
|
| 23 |
+
|
| 24 |
+
const headersRaw = (entry as Record<string, unknown>).headers;
|
| 25 |
+
let headers: Record<string, string> | undefined;
|
| 26 |
+
if (headersRaw && typeof headersRaw === "object" && !Array.isArray(headersRaw)) {
|
| 27 |
+
const headerEntries = Object.entries(headersRaw as Record<string, unknown>).filter(
|
| 28 |
+
(entry): entry is [string, string] => typeof entry[1] === "string"
|
| 29 |
+
);
|
| 30 |
+
headers = Object.fromEntries(headerEntries);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return headers ? { name, url, headers } : { name, url };
|
| 34 |
+
})
|
| 35 |
+
.filter((server): server is McpServerConfig => Boolean(server));
|
| 36 |
+
} catch (error) {
|
| 37 |
+
logger.warn({ err: error }, "[mcp] failed to parse MCP_SERVERS env");
|
| 38 |
+
return [];
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function setServers(raw: string) {
|
| 43 |
+
cachedServers = parseServers(raw);
|
| 44 |
+
cachedRaw = raw;
|
| 45 |
+
resetMcpToolsCache();
|
| 46 |
+
logger.debug({ count: cachedServers.length }, "[mcp] loaded server configuration");
|
| 47 |
+
console.log(
|
| 48 |
+
`[MCP] Loaded ${cachedServers.length} server(s):`,
|
| 49 |
+
cachedServers.map((s) => s.name).join(", ") || "none"
|
| 50 |
+
);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export function loadMcpServersOnStartup(): McpServerConfig[] {
|
| 54 |
+
const raw = config.MCP_SERVERS || "[]";
|
| 55 |
+
setServers(raw);
|
| 56 |
+
return cachedServers;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function refreshMcpServersIfChanged(): void {
|
| 60 |
+
const currentRaw = config.MCP_SERVERS || "[]";
|
| 61 |
+
if (cachedRaw === null) {
|
| 62 |
+
setServers(currentRaw);
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if (currentRaw !== cachedRaw) {
|
| 67 |
+
setServers(currentRaw);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export function getMcpServers(): McpServerConfig[] {
|
| 72 |
+
if (cachedRaw === null) {
|
| 73 |
+
loadMcpServersOnStartup();
|
| 74 |
+
}
|
| 75 |
+
return cachedServers;
|
| 76 |
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
| 2 |
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
| 3 |
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
| 4 |
+
import type { McpServerConfig } from "./httpClient";
|
| 5 |
+
|
| 6 |
+
export type OpenAiTool = {
|
| 7 |
+
type: "function";
|
| 8 |
+
function: { name: string; description?: string; parameters?: Record<string, unknown> };
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export interface McpToolMapping {
|
| 12 |
+
fnName: string;
|
| 13 |
+
server: string;
|
| 14 |
+
tool: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface CacheEntry {
|
| 18 |
+
fetchedAt: number;
|
| 19 |
+
ttlMs: number;
|
| 20 |
+
tools: OpenAiTool[];
|
| 21 |
+
mapping: Record<string, McpToolMapping>;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const DEFAULT_TTL_MS = 60_000;
|
| 25 |
+
const cache = new Map<string, CacheEntry>();
|
| 26 |
+
|
| 27 |
+
// Per OpenAI tool/function name guidelines most providers enforce:
|
| 28 |
+
// ^[a-zA-Z0-9_-]{1,64}$
|
| 29 |
+
// Dots are not universally accepted (e.g., MiniMax via HF router rejects them).
|
| 30 |
+
// Normalize any disallowed characters (including ".") to underscore and trim to 64 chars.
|
| 31 |
+
function sanitizeName(name: string) {
|
| 32 |
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function buildCacheKey(servers: McpServerConfig[]): string {
|
| 36 |
+
const normalized = servers
|
| 37 |
+
.map((server) => ({
|
| 38 |
+
name: server.name,
|
| 39 |
+
url: server.url,
|
| 40 |
+
headers: server.headers
|
| 41 |
+
? Object.entries(server.headers)
|
| 42 |
+
.sort(([a], [b]) => a.localeCompare(b))
|
| 43 |
+
.map(([key, value]) => [key, value])
|
| 44 |
+
: [],
|
| 45 |
+
}))
|
| 46 |
+
.sort((a, b) => {
|
| 47 |
+
const byName = a.name.localeCompare(b.name);
|
| 48 |
+
if (byName !== 0) return byName;
|
| 49 |
+
return a.url.localeCompare(b.url);
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
return JSON.stringify(normalized);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
type ListedTool = {
|
| 56 |
+
name?: string;
|
| 57 |
+
inputSchema?: Record<string, unknown>;
|
| 58 |
+
description?: string;
|
| 59 |
+
annotations?: { title?: string };
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
async function listServerTools(
|
| 63 |
+
server: McpServerConfig,
|
| 64 |
+
opts: { signal?: AbortSignal } = {}
|
| 65 |
+
): Promise<ListedTool[]> {
|
| 66 |
+
const url = new URL(server.url);
|
| 67 |
+
const client = new Client({ name: "chat-ui-mcp", version: "0.1.0" });
|
| 68 |
+
try {
|
| 69 |
+
try {
|
| 70 |
+
const transport = new StreamableHTTPClientTransport(url, {
|
| 71 |
+
requestInit: { headers: server.headers, signal: opts.signal },
|
| 72 |
+
});
|
| 73 |
+
await client.connect(transport);
|
| 74 |
+
} catch {
|
| 75 |
+
const transport = new SSEClientTransport(url, {
|
| 76 |
+
requestInit: { headers: server.headers, signal: opts.signal },
|
| 77 |
+
});
|
| 78 |
+
await client.connect(transport);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
const response = await client.listTools({});
|
| 82 |
+
return Array.isArray(response?.tools) ? (response.tools as ListedTool[]) : [];
|
| 83 |
+
} finally {
|
| 84 |
+
try {
|
| 85 |
+
await client.close?.();
|
| 86 |
+
} catch {
|
| 87 |
+
// ignore close errors
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export async function getOpenAiToolsForMcp(
|
| 93 |
+
servers: McpServerConfig[],
|
| 94 |
+
{ ttlMs = DEFAULT_TTL_MS, signal }: { ttlMs?: number; signal?: AbortSignal } = {}
|
| 95 |
+
): Promise<{ tools: OpenAiTool[]; mapping: Record<string, McpToolMapping> }> {
|
| 96 |
+
const now = Date.now();
|
| 97 |
+
const cacheKey = buildCacheKey(servers);
|
| 98 |
+
const cached = cache.get(cacheKey);
|
| 99 |
+
if (cached && now - cached.fetchedAt < cached.ttlMs) {
|
| 100 |
+
return { tools: cached.tools, mapping: cached.mapping };
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const tools: OpenAiTool[] = [];
|
| 104 |
+
const mapping: Record<string, McpToolMapping> = {};
|
| 105 |
+
|
| 106 |
+
const seenNames = new Set<string>();
|
| 107 |
+
|
| 108 |
+
const pushToolDefinition = (
|
| 109 |
+
name: string,
|
| 110 |
+
description: string | undefined,
|
| 111 |
+
parameters: Record<string, unknown> | undefined
|
| 112 |
+
) => {
|
| 113 |
+
if (seenNames.has(name)) return;
|
| 114 |
+
tools.push({
|
| 115 |
+
type: "function",
|
| 116 |
+
function: {
|
| 117 |
+
name,
|
| 118 |
+
description,
|
| 119 |
+
parameters,
|
| 120 |
+
},
|
| 121 |
+
});
|
| 122 |
+
seenNames.add(name);
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
// Fetch tools in parallel; tolerate individual failures
|
| 126 |
+
const tasks = servers.map((server) => listServerTools(server, { signal }));
|
| 127 |
+
const results = await Promise.allSettled(tasks);
|
| 128 |
+
|
| 129 |
+
for (let i = 0; i < results.length; i++) {
|
| 130 |
+
const server = servers[i];
|
| 131 |
+
const r = results[i];
|
| 132 |
+
if (r.status === "fulfilled") {
|
| 133 |
+
const serverTools = r.value;
|
| 134 |
+
for (const tool of serverTools) {
|
| 135 |
+
if (typeof tool.name !== "string" || tool.name.trim().length === 0) {
|
| 136 |
+
continue;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const parameters =
|
| 140 |
+
tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : undefined;
|
| 141 |
+
const description = tool.description ?? tool.annotations?.title;
|
| 142 |
+
const toolName = tool.name;
|
| 143 |
+
|
| 144 |
+
// Emit a collision-aware function name.
|
| 145 |
+
// Prefer the plain tool name; on conflict, suffix with server name.
|
| 146 |
+
let plainName = sanitizeName(toolName);
|
| 147 |
+
if (plainName in mapping) {
|
| 148 |
+
const suffix = sanitizeName(server.name);
|
| 149 |
+
const candidate = `${plainName}_${suffix}`.slice(0, 64);
|
| 150 |
+
if (!(candidate in mapping)) {
|
| 151 |
+
plainName = candidate;
|
| 152 |
+
} else {
|
| 153 |
+
let i = 2;
|
| 154 |
+
let next = `${candidate}_${i}`;
|
| 155 |
+
while (i < 10 && next in mapping) {
|
| 156 |
+
i += 1;
|
| 157 |
+
next = `${candidate}_${i}`;
|
| 158 |
+
}
|
| 159 |
+
plainName = next.slice(0, 64);
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
pushToolDefinition(plainName, description, parameters);
|
| 164 |
+
mapping[plainName] = {
|
| 165 |
+
fnName: plainName,
|
| 166 |
+
server: server.name,
|
| 167 |
+
tool: toolName,
|
| 168 |
+
};
|
| 169 |
+
}
|
| 170 |
+
} else {
|
| 171 |
+
// ignore failure for this server
|
| 172 |
+
continue;
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
cache.set(cacheKey, { fetchedAt: now, ttlMs, tools, mapping });
|
| 177 |
+
return { tools, mapping };
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
export function resetMcpToolsCache() {
|
| 181 |
+
cache.clear();
|
| 182 |
+
}
|
|
@@ -56,6 +56,8 @@ const modelConfig = z.object({
|
|
| 56 |
.optional(),
|
| 57 |
multimodal: z.boolean().default(false),
|
| 58 |
multimodalAcceptedMimetypes: z.array(z.string()).optional(),
|
|
|
|
|
|
|
| 59 |
unlisted: z.boolean().default(false),
|
| 60 |
embeddingModel: z.never().optional(),
|
| 61 |
/** Used to enable/disable system prompt usage */
|
|
@@ -234,6 +236,7 @@ const signatureForModel = (model: ProcessedModel) =>
|
|
| 234 |
}) ?? null,
|
| 235 |
multimodal: model.multimodal,
|
| 236 |
multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
|
|
|
|
| 237 |
isRouter: model.isRouter,
|
| 238 |
hasInferenceAPI: model.hasInferenceAPI,
|
| 239 |
});
|
|
@@ -341,6 +344,9 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
|
|
| 341 |
);
|
| 342 |
const supportsImageInput =
|
| 343 |
inputModalities.includes("image") || inputModalities.includes("vision");
|
|
|
|
|
|
|
|
|
|
| 344 |
return {
|
| 345 |
id: m.id,
|
| 346 |
name: m.id,
|
|
@@ -350,6 +356,7 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
|
|
| 350 |
providers: m.providers,
|
| 351 |
multimodal: supportsImageInput,
|
| 352 |
multimodalAcceptedMimetypes: supportsImageInput ? ["image/*"] : undefined,
|
|
|
|
| 353 |
endpoints: [
|
| 354 |
{
|
| 355 |
type: "openai" as const,
|
|
@@ -405,6 +412,7 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
|
|
| 405 |
const routerAliasId = (config.PUBLIC_LLM_ROUTER_ALIAS_ID || "omni").trim() || "omni";
|
| 406 |
const routerMultimodalEnabled =
|
| 407 |
(config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
|
|
|
|
| 408 |
|
| 409 |
let decorated = builtModels as ProcessedModel[];
|
| 410 |
|
|
@@ -432,6 +440,10 @@ const buildModels = async (): Promise<ProcessedModel[]> => {
|
|
| 432 |
aliasRaw.multimodalAcceptedMimetypes = ["image/*"];
|
| 433 |
}
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
const aliasBase = await processModel(aliasRaw);
|
| 436 |
// Create a self-referential ProcessedModel for the router endpoint
|
| 437 |
const aliasModel: ProcessedModel = {
|
|
|
|
| 56 |
.optional(),
|
| 57 |
multimodal: z.boolean().default(false),
|
| 58 |
multimodalAcceptedMimetypes: z.array(z.string()).optional(),
|
| 59 |
+
// Aggregated tool-calling capability across providers (HF router)
|
| 60 |
+
supportsTools: z.boolean().default(false),
|
| 61 |
unlisted: z.boolean().default(false),
|
| 62 |
embeddingModel: z.never().optional(),
|
| 63 |
/** Used to enable/disable system prompt usage */
|
|
|
|
| 236 |
}) ?? null,
|
| 237 |
multimodal: model.multimodal,
|
| 238 |
multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
|
| 239 |
+
supportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,
|
| 240 |
isRouter: model.isRouter,
|
| 241 |
hasInferenceAPI: model.hasInferenceAPI,
|
| 242 |
});
|
|
|
|
| 344 |
);
|
| 345 |
const supportsImageInput =
|
| 346 |
inputModalities.includes("image") || inputModalities.includes("vision");
|
| 347 |
+
|
| 348 |
+
// If any provider supports tools, consider the model as supporting tools
|
| 349 |
+
const supportsTools = Boolean((m.providers ?? []).some((p) => p?.supports_tools === true));
|
| 350 |
return {
|
| 351 |
id: m.id,
|
| 352 |
name: m.id,
|
|
|
|
| 356 |
providers: m.providers,
|
| 357 |
multimodal: supportsImageInput,
|
| 358 |
multimodalAcceptedMimetypes: supportsImageInput ? ["image/*"] : undefined,
|
| 359 |
+
supportsTools,
|
| 360 |
endpoints: [
|
| 361 |
{
|
| 362 |
type: "openai" as const,
|
|
|
|
| 412 |
const routerAliasId = (config.PUBLIC_LLM_ROUTER_ALIAS_ID || "omni").trim() || "omni";
|
| 413 |
const routerMultimodalEnabled =
|
| 414 |
(config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
|
| 415 |
+
const routerToolsEnabled = (config.LLM_ROUTER_ENABLE_TOOLS || "").toLowerCase() === "true";
|
| 416 |
|
| 417 |
let decorated = builtModels as ProcessedModel[];
|
| 418 |
|
|
|
|
| 440 |
aliasRaw.multimodalAcceptedMimetypes = ["image/*"];
|
| 441 |
}
|
| 442 |
|
| 443 |
+
if (routerToolsEnabled) {
|
| 444 |
+
aliasRaw.supportsTools = true;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
const aliasBase = await processModel(aliasRaw);
|
| 448 |
// Create a self-referential ProcessedModel for the router endpoint
|
| 449 |
const aliasModel: ProcessedModel = {
|
|
@@ -12,6 +12,12 @@ import { archSelectRoute } from "./arch";
|
|
| 12 |
import { getRoutes, resolveRouteModels } from "./policy";
|
| 13 |
import { getApiToken } from "$lib/server/apiToken";
|
| 14 |
import { ROUTER_FAILURE } from "./types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
const REASONING_BLOCK_REGEX = /<think>[\s\S]*?(?:<\/think>|$)/g;
|
| 17 |
|
|
@@ -115,11 +121,14 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
|
|
| 115 |
const sanitizedMessages = params.messages.map(stripReasoningFromMessage);
|
| 116 |
const routerMultimodalEnabled =
|
| 117 |
(config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
|
|
|
|
| 118 |
const hasImageInput = sanitizedMessages.some((message) =>
|
| 119 |
(message.files ?? []).some(
|
| 120 |
(file) => typeof file?.mime === "string" && file.mime.startsWith("image/")
|
| 121 |
)
|
| 122 |
);
|
|
|
|
|
|
|
| 123 |
|
| 124 |
// Helper to create an OpenAI endpoint for a specific candidate model id
|
| 125 |
async function createCandidateEndpoint(candidateModelId: string): Promise<Endpoint> {
|
|
@@ -230,6 +239,46 @@ export async function makeRouterEndpoint(routerModel: ProcessedModel): Promise<E
|
|
| 230 |
}
|
| 231 |
}
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
const routeSelection = await archSelectRoute(sanitizedMessages, undefined, params.locals);
|
| 234 |
|
| 235 |
// If arch router failed with an error, only hard-fail for policy errors (402/401/403)
|
|
|
|
| 12 |
import { getRoutes, resolveRouteModels } from "./policy";
|
| 13 |
import { getApiToken } from "$lib/server/apiToken";
|
| 14 |
import { ROUTER_FAILURE } from "./types";
|
| 15 |
+
import {
|
| 16 |
+
hasActiveToolsSelection,
|
| 17 |
+
isRouterToolsBypassEnabled,
|
| 18 |
+
pickToolsCapableModel,
|
| 19 |
+
ROUTER_TOOLS_ROUTE,
|
| 20 |
+
} from "./toolsRoute";
|
| 21 |
|
| 22 |
const REASONING_BLOCK_REGEX = /<think>[\s\S]*?(?:<\/think>|$)/g;
|
| 23 |
|
|
|
|
| 121 |
const sanitizedMessages = params.messages.map(stripReasoningFromMessage);
|
| 122 |
const routerMultimodalEnabled =
|
| 123 |
(config.LLM_ROUTER_ENABLE_MULTIMODAL || "").toLowerCase() === "true";
|
| 124 |
+
const routerToolsEnabled = isRouterToolsBypassEnabled();
|
| 125 |
const hasImageInput = sanitizedMessages.some((message) =>
|
| 126 |
(message.files ?? []).some(
|
| 127 |
(file) => typeof file?.mime === "string" && file.mime.startsWith("image/")
|
| 128 |
)
|
| 129 |
);
|
| 130 |
+
// Tools are considered "active" if the client indicated any enabled MCP server
|
| 131 |
+
const hasToolsActive = hasActiveToolsSelection(params.locals);
|
| 132 |
|
| 133 |
// Helper to create an OpenAI endpoint for a specific candidate model id
|
| 134 |
async function createCandidateEndpoint(candidateModelId: string): Promise<Endpoint> {
|
|
|
|
| 239 |
}
|
| 240 |
}
|
| 241 |
|
| 242 |
+
async function findToolsCandidateModel(): Promise<ProcessedModel | undefined> {
|
| 243 |
+
try {
|
| 244 |
+
const all = await getModels();
|
| 245 |
+
return pickToolsCapableModel(all);
|
| 246 |
+
} catch (e) {
|
| 247 |
+
logger.warn({ err: String(e) }, "[router] failed to load models for tools lookup");
|
| 248 |
+
return undefined;
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
if (routerToolsEnabled && hasToolsActive) {
|
| 253 |
+
const toolsModel = await findToolsCandidateModel();
|
| 254 |
+
const toolsCandidate = toolsModel?.id ?? toolsModel?.name;
|
| 255 |
+
if (!toolsCandidate) {
|
| 256 |
+
// No tool-capable model found — continue with normal routing instead of hard failing
|
| 257 |
+
} else {
|
| 258 |
+
try {
|
| 259 |
+
logger.info(
|
| 260 |
+
{ route: ROUTER_TOOLS_ROUTE, model: toolsCandidate },
|
| 261 |
+
"[router] tools active; bypassing Arch selection"
|
| 262 |
+
);
|
| 263 |
+
const ep = await createCandidateEndpoint(toolsCandidate);
|
| 264 |
+
const gen = await ep({ ...params });
|
| 265 |
+
return metadataThenStream(gen, toolsCandidate, ROUTER_TOOLS_ROUTE);
|
| 266 |
+
} catch (e) {
|
| 267 |
+
const { message, statusCode } = extractUpstreamError(e);
|
| 268 |
+
logger.error(
|
| 269 |
+
{
|
| 270 |
+
route: ROUTER_TOOLS_ROUTE,
|
| 271 |
+
model: toolsCandidate,
|
| 272 |
+
err: message,
|
| 273 |
+
...(statusCode && { status: statusCode }),
|
| 274 |
+
},
|
| 275 |
+
"[router] tools fallback failed"
|
| 276 |
+
);
|
| 277 |
+
throw statusCode ? new HTTPError(message, statusCode) : new Error(message);
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
const routeSelection = await archSelectRoute(sanitizedMessages, undefined, params.locals);
|
| 283 |
|
| 284 |
// If arch router failed with an error, only hard-fail for policy errors (402/401/403)
|
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
+
import { logger } from "$lib/server/logger";
|
| 3 |
+
import type { ProcessedModel } from "../models";
|
| 4 |
+
|
| 5 |
+
export const ROUTER_TOOLS_ROUTE = "agentic";
|
| 6 |
+
|
| 7 |
+
type LocalsWithMcp = App.Locals & {
|
| 8 |
+
mcp?: {
|
| 9 |
+
selectedServers?: unknown[];
|
| 10 |
+
selectedServerNames?: unknown[];
|
| 11 |
+
};
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export function isRouterToolsBypassEnabled(): boolean {
|
| 15 |
+
return (config.LLM_ROUTER_ENABLE_TOOLS || "").toLowerCase() === "true";
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function hasActiveToolsSelection(locals: App.Locals | undefined): boolean {
|
| 19 |
+
try {
|
| 20 |
+
const reqMcp = (locals as LocalsWithMcp | undefined)?.mcp;
|
| 21 |
+
const byConfig =
|
| 22 |
+
Array.isArray(reqMcp?.selectedServers) && (reqMcp?.selectedServers?.length ?? 0) > 0;
|
| 23 |
+
const byName =
|
| 24 |
+
Array.isArray(reqMcp?.selectedServerNames) && (reqMcp?.selectedServerNames?.length ?? 0) > 0;
|
| 25 |
+
return Boolean(byConfig || byName);
|
| 26 |
+
} catch {
|
| 27 |
+
return false;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function pickToolsCapableModel(
|
| 32 |
+
models: ProcessedModel[] | undefined
|
| 33 |
+
): ProcessedModel | undefined {
|
| 34 |
+
const preferredRaw = (config as unknown as Record<string, string>).LLM_ROUTER_TOOLS_MODEL;
|
| 35 |
+
const preferred = preferredRaw?.trim();
|
| 36 |
+
if (!preferred) {
|
| 37 |
+
logger.warn("[router] tools bypass requested but LLM_ROUTER_TOOLS_MODEL is not set");
|
| 38 |
+
return undefined;
|
| 39 |
+
}
|
| 40 |
+
if (!models?.length) return undefined;
|
| 41 |
+
const found = models.find((m) => m.id === preferred || m.name === preferred);
|
| 42 |
+
if (!found) {
|
| 43 |
+
logger.warn(
|
| 44 |
+
{ configuredModel: preferred },
|
| 45 |
+
"[router] configured tools model not found; falling back to Arch routing"
|
| 46 |
+
);
|
| 47 |
+
return undefined;
|
| 48 |
+
}
|
| 49 |
+
logger.info({ model: found.id ?? found.name }, "[router] using configured tools model");
|
| 50 |
+
return found;
|
| 51 |
+
}
|
|
@@ -1,7 +1,14 @@
|
|
| 1 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import { AbortedGenerations } from "../abortedGenerations";
|
| 3 |
import type { TextGenerationContext } from "./types";
|
| 4 |
import type { EndpointMessage } from "../endpoints/endpoints";
|
|
|
|
|
|
|
| 5 |
import { logger } from "../logger";
|
| 6 |
|
| 7 |
type GenerateContext = Omit<TextGenerationContext, "messages"> & { messages: EndpointMessage[] };
|
|
@@ -20,6 +27,30 @@ export async function* generate(
|
|
| 20 |
}: GenerateContext,
|
| 21 |
preprompt?: string
|
| 22 |
): AsyncIterable<MessageUpdate> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const stream = await endpoint({
|
| 24 |
messages,
|
| 25 |
preprompt,
|
|
@@ -32,20 +63,24 @@ export async function* generate(
|
|
| 32 |
});
|
| 33 |
|
| 34 |
for await (const output of stream) {
|
| 35 |
-
// Check if this output contains router metadata
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
(
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
// text generation completed
|
| 51 |
if (output.generated_text) {
|
|
@@ -60,19 +95,139 @@ export async function* generate(
|
|
| 60 |
text = text.slice(0, text.length - stopToken.length);
|
| 61 |
}
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
continue;
|
| 69 |
}
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
// ignore special tokens
|
| 72 |
if (output.token.special) continue;
|
| 73 |
|
| 74 |
-
//
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
// abort check
|
| 78 |
const date = AbortedGenerations.getInstance().getAbortTime(conv._id.toString());
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
+
import {
|
| 3 |
+
MessageReasoningUpdateType,
|
| 4 |
+
MessageUpdateType,
|
| 5 |
+
type MessageUpdate,
|
| 6 |
+
} from "$lib/types/MessageUpdate";
|
| 7 |
import { AbortedGenerations } from "../abortedGenerations";
|
| 8 |
import type { TextGenerationContext } from "./types";
|
| 9 |
import type { EndpointMessage } from "../endpoints/endpoints";
|
| 10 |
+
import { generateFromDefaultEndpoint } from "../generateFromDefaultEndpoint";
|
| 11 |
+
import { generateSummaryOfReasoning } from "./reasoning";
|
| 12 |
import { logger } from "../logger";
|
| 13 |
|
| 14 |
type GenerateContext = Omit<TextGenerationContext, "messages"> & { messages: EndpointMessage[] };
|
|
|
|
| 27 |
}: GenerateContext,
|
| 28 |
preprompt?: string
|
| 29 |
): AsyncIterable<MessageUpdate> {
|
| 30 |
+
// Reasoning mode support
|
| 31 |
+
let reasoning = false;
|
| 32 |
+
let reasoningBuffer = "";
|
| 33 |
+
let lastReasoningUpdate = new Date();
|
| 34 |
+
let status = "";
|
| 35 |
+
const startTime = new Date();
|
| 36 |
+
const modelReasoning = Reflect.get(model, "reasoning") as
|
| 37 |
+
| { type: string; beginToken?: string; endToken?: string; regex?: string }
|
| 38 |
+
| undefined;
|
| 39 |
+
if (
|
| 40 |
+
modelReasoning &&
|
| 41 |
+
(modelReasoning.type === "regex" ||
|
| 42 |
+
modelReasoning.type === "summarize" ||
|
| 43 |
+
(modelReasoning.type === "tokens" && modelReasoning.beginToken === ""))
|
| 44 |
+
) {
|
| 45 |
+
// Starts in reasoning mode and we extract the answer from the reasoning
|
| 46 |
+
reasoning = true;
|
| 47 |
+
yield {
|
| 48 |
+
type: MessageUpdateType.Reasoning,
|
| 49 |
+
subtype: MessageReasoningUpdateType.Status,
|
| 50 |
+
status: "Started reasoning...",
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
const stream = await endpoint({
|
| 55 |
messages,
|
| 56 |
preprompt,
|
|
|
|
| 63 |
});
|
| 64 |
|
| 65 |
for await (const output of stream) {
|
| 66 |
+
// Check if this output contains router metadata. Emit if either:
|
| 67 |
+
// 1) route+model are present (router models), or
|
| 68 |
+
// 2) provider-only is present (non-router models exposing x-inference-provider)
|
| 69 |
+
if ("routerMetadata" in output && output.routerMetadata) {
|
| 70 |
+
const hasRouteModel = Boolean(output.routerMetadata.route && output.routerMetadata.model);
|
| 71 |
+
const hasProviderOnly = Boolean(output.routerMetadata.provider);
|
| 72 |
+
if (hasRouteModel || hasProviderOnly) {
|
| 73 |
+
yield {
|
| 74 |
+
type: MessageUpdateType.RouterMetadata,
|
| 75 |
+
route: output.routerMetadata.route || "",
|
| 76 |
+
model: output.routerMetadata.model || "",
|
| 77 |
+
provider:
|
| 78 |
+
(output.routerMetadata
|
| 79 |
+
.provider as unknown as import("@huggingface/inference").InferenceProvider) ||
|
| 80 |
+
undefined,
|
| 81 |
+
};
|
| 82 |
+
continue;
|
| 83 |
+
}
|
| 84 |
}
|
| 85 |
// text generation completed
|
| 86 |
if (output.generated_text) {
|
|
|
|
| 95 |
text = text.slice(0, text.length - stopToken.length);
|
| 96 |
}
|
| 97 |
|
| 98 |
+
let finalAnswer = text;
|
| 99 |
+
if (modelReasoning && modelReasoning.type === "regex" && modelReasoning.regex) {
|
| 100 |
+
const regex = new RegExp(modelReasoning.regex);
|
| 101 |
+
finalAnswer = regex.exec(reasoningBuffer)?.[1] ?? text;
|
| 102 |
+
} else if (modelReasoning && modelReasoning.type === "summarize") {
|
| 103 |
+
yield {
|
| 104 |
+
type: MessageUpdateType.Reasoning,
|
| 105 |
+
subtype: MessageReasoningUpdateType.Status,
|
| 106 |
+
status: "Summarizing reasoning...",
|
| 107 |
+
};
|
| 108 |
+
try {
|
| 109 |
+
const summary = yield* generateFromDefaultEndpoint({
|
| 110 |
+
messages: [
|
| 111 |
+
{
|
| 112 |
+
from: "user",
|
| 113 |
+
content: `Question: ${messages[messages.length - 1].content}\n\nReasoning: ${reasoningBuffer}`,
|
| 114 |
+
},
|
| 115 |
+
],
|
| 116 |
+
preprompt: `Your task is to summarize concisely all your reasoning steps and then give the final answer. Keep it short, one short paragraph at most. If the reasoning steps explicitly include a code solution, make sure to include it in your answer.`,
|
| 117 |
+
modelId: Reflect.get(model, "id") as string | undefined,
|
| 118 |
+
locals,
|
| 119 |
+
});
|
| 120 |
+
finalAnswer = summary;
|
| 121 |
+
yield {
|
| 122 |
+
type: MessageUpdateType.Reasoning,
|
| 123 |
+
subtype: MessageReasoningUpdateType.Status,
|
| 124 |
+
status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`,
|
| 125 |
+
};
|
| 126 |
+
} catch (e) {
|
| 127 |
+
finalAnswer = text;
|
| 128 |
+
logger.error(e);
|
| 129 |
+
}
|
| 130 |
+
} else if (modelReasoning && modelReasoning.type === "tokens") {
|
| 131 |
+
// Remove the reasoning segment from final answer to avoid duplication
|
| 132 |
+
const beginIndex = modelReasoning.beginToken
|
| 133 |
+
? reasoningBuffer.indexOf(modelReasoning.beginToken)
|
| 134 |
+
: 0;
|
| 135 |
+
const endIndex = modelReasoning.endToken
|
| 136 |
+
? reasoningBuffer.lastIndexOf(modelReasoning.endToken)
|
| 137 |
+
: -1;
|
| 138 |
+
|
| 139 |
+
if (beginIndex !== -1 && endIndex !== -1 && modelReasoning.endToken) {
|
| 140 |
+
finalAnswer =
|
| 141 |
+
text.slice(0, beginIndex) + text.slice(endIndex + modelReasoning.endToken.length);
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
yield { type: MessageUpdateType.FinalAnswer, text: finalAnswer, interrupted };
|
| 146 |
continue;
|
| 147 |
}
|
| 148 |
|
| 149 |
+
if (modelReasoning && modelReasoning.type === "tokens") {
|
| 150 |
+
if (output.token.text === modelReasoning.beginToken) {
|
| 151 |
+
reasoning = true;
|
| 152 |
+
reasoningBuffer += output.token.text;
|
| 153 |
+
continue;
|
| 154 |
+
} else if (modelReasoning.endToken && output.token.text === modelReasoning.endToken) {
|
| 155 |
+
reasoning = false;
|
| 156 |
+
reasoningBuffer += output.token.text;
|
| 157 |
+
yield {
|
| 158 |
+
type: MessageUpdateType.Reasoning,
|
| 159 |
+
subtype: MessageReasoningUpdateType.Status,
|
| 160 |
+
status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`,
|
| 161 |
+
};
|
| 162 |
+
continue;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
// ignore special tokens
|
| 167 |
if (output.token.special) continue;
|
| 168 |
|
| 169 |
+
// pass down normal token
|
| 170 |
+
if (reasoning) {
|
| 171 |
+
reasoningBuffer += output.token.text;
|
| 172 |
+
|
| 173 |
+
if (modelReasoning && modelReasoning.type === "tokens" && modelReasoning.endToken) {
|
| 174 |
+
if (reasoningBuffer.lastIndexOf(modelReasoning.endToken) !== -1) {
|
| 175 |
+
const endTokenIndex = reasoningBuffer.lastIndexOf(modelReasoning.endToken);
|
| 176 |
+
const textBuffer = reasoningBuffer.slice(endTokenIndex + modelReasoning.endToken.length);
|
| 177 |
+
reasoningBuffer = reasoningBuffer.slice(
|
| 178 |
+
0,
|
| 179 |
+
endTokenIndex + modelReasoning.endToken.length + 1
|
| 180 |
+
);
|
| 181 |
+
|
| 182 |
+
yield {
|
| 183 |
+
type: MessageUpdateType.Reasoning,
|
| 184 |
+
subtype: MessageReasoningUpdateType.Stream,
|
| 185 |
+
token: output.token.text,
|
| 186 |
+
};
|
| 187 |
+
yield { type: MessageUpdateType.Stream, token: textBuffer };
|
| 188 |
+
yield {
|
| 189 |
+
type: MessageUpdateType.Reasoning,
|
| 190 |
+
subtype: MessageReasoningUpdateType.Status,
|
| 191 |
+
status: `Done in ${Math.round((new Date().getTime() - startTime.getTime()) / 1000)}s.`,
|
| 192 |
+
};
|
| 193 |
+
reasoning = false;
|
| 194 |
+
continue;
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// yield status update if it has changed
|
| 199 |
+
if (status !== "") {
|
| 200 |
+
yield {
|
| 201 |
+
type: MessageUpdateType.Reasoning,
|
| 202 |
+
subtype: MessageReasoningUpdateType.Status,
|
| 203 |
+
status,
|
| 204 |
+
};
|
| 205 |
+
status = "";
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// create a new status every ~4s (optional)
|
| 209 |
+
if (
|
| 210 |
+
Reflect.get(config, "REASONING_SUMMARY") === "true" &&
|
| 211 |
+
new Date().getTime() - lastReasoningUpdate.getTime() > 4000
|
| 212 |
+
) {
|
| 213 |
+
lastReasoningUpdate = new Date();
|
| 214 |
+
try {
|
| 215 |
+
generateSummaryOfReasoning(reasoningBuffer, model.id, locals).then((summary) => {
|
| 216 |
+
status = summary;
|
| 217 |
+
});
|
| 218 |
+
} catch (e) {
|
| 219 |
+
logger.error(e);
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
yield {
|
| 224 |
+
type: MessageUpdateType.Reasoning,
|
| 225 |
+
subtype: MessageReasoningUpdateType.Stream,
|
| 226 |
+
token: output.token.text,
|
| 227 |
+
};
|
| 228 |
+
} else {
|
| 229 |
+
yield { type: MessageUpdateType.Stream, token: output.token.text };
|
| 230 |
+
}
|
| 231 |
|
| 232 |
// abort check
|
| 233 |
const date = AbortedGenerations.getInstance().getAbortTime(conv._id.toString());
|
|
@@ -7,6 +7,7 @@ import {
|
|
| 7 |
MessageUpdateStatus,
|
| 8 |
} from "$lib/types/MessageUpdate";
|
| 9 |
import { generate } from "./generate";
|
|
|
|
| 10 |
import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
|
| 11 |
import type { TextGenerationContext } from "./types";
|
| 12 |
|
|
@@ -47,6 +48,34 @@ async function* textGenerationWithoutTitle(
|
|
| 47 |
const preprompt = conv.preprompt;
|
| 48 |
|
| 49 |
const processedMessages = await preprocessMessages(messages, convId);
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
done.abort();
|
| 52 |
}
|
|
|
|
| 7 |
MessageUpdateStatus,
|
| 8 |
} from "$lib/types/MessageUpdate";
|
| 9 |
import { generate } from "./generate";
|
| 10 |
+
import { runMcpFlow } from "./mcp/runMcpFlow";
|
| 11 |
import { mergeAsyncGenerators } from "$lib/utils/mergeAsyncGenerators";
|
| 12 |
import type { TextGenerationContext } from "./types";
|
| 13 |
|
|
|
|
| 48 |
const preprompt = conv.preprompt;
|
| 49 |
|
| 50 |
const processedMessages = await preprocessMessages(messages, convId);
|
| 51 |
+
|
| 52 |
+
// Try MCP tool flow first; fall back to default generation if not selected/available
|
| 53 |
+
try {
|
| 54 |
+
const mcpGen = runMcpFlow({
|
| 55 |
+
model: ctx.model,
|
| 56 |
+
conv,
|
| 57 |
+
messages: processedMessages,
|
| 58 |
+
assistant: ctx.assistant,
|
| 59 |
+
forceMultimodal: ctx.forceMultimodal,
|
| 60 |
+
forceTools: ctx.forceTools,
|
| 61 |
+
locals: ctx.locals,
|
| 62 |
+
preprompt,
|
| 63 |
+
abortSignal: ctx.abortController.signal,
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
let step = await mcpGen.next();
|
| 67 |
+
while (!step.done) {
|
| 68 |
+
yield step.value;
|
| 69 |
+
step = await mcpGen.next();
|
| 70 |
+
}
|
| 71 |
+
const didRunMcp = Boolean(step.value);
|
| 72 |
+
if (!didRunMcp) {
|
| 73 |
+
// fallback to normal text generation
|
| 74 |
+
yield* generate({ ...ctx, messages: processedMessages }, preprompt);
|
| 75 |
+
}
|
| 76 |
+
} catch {
|
| 77 |
+
// On any MCP error, fall back to normal generation
|
| 78 |
+
yield* generate({ ...ctx, messages: processedMessages }, preprompt);
|
| 79 |
+
}
|
| 80 |
done.abort();
|
| 81 |
}
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
+
import { archSelectRoute } from "$lib/server/router/arch";
|
| 3 |
+
import { getRoutes, resolveRouteModels } from "$lib/server/router/policy";
|
| 4 |
+
import {
|
| 5 |
+
hasActiveToolsSelection,
|
| 6 |
+
isRouterToolsBypassEnabled,
|
| 7 |
+
pickToolsCapableModel,
|
| 8 |
+
ROUTER_TOOLS_ROUTE,
|
| 9 |
+
} from "$lib/server/router/toolsRoute";
|
| 10 |
+
import type { EndpointMessage } from "../../endpoints/endpoints";
|
| 11 |
+
import { stripReasoningFromMessageForRouting } from "../utils/routing";
|
| 12 |
+
import type { ProcessedModel } from "../../models";
|
| 13 |
+
import { logger } from "../../logger";
|
| 14 |
+
|
| 15 |
+
export interface RouterResolutionInput {
|
| 16 |
+
model: ProcessedModel;
|
| 17 |
+
messages: EndpointMessage[];
|
| 18 |
+
conversationId: string;
|
| 19 |
+
hasImageInput: boolean;
|
| 20 |
+
locals: App.Locals | undefined;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface RouterResolutionResult {
|
| 24 |
+
runMcp: boolean;
|
| 25 |
+
targetModel: ProcessedModel;
|
| 26 |
+
candidateModelId?: string;
|
| 27 |
+
resolvedRoute?: string;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export async function resolveRouterTarget({
|
| 31 |
+
model,
|
| 32 |
+
messages,
|
| 33 |
+
conversationId,
|
| 34 |
+
hasImageInput,
|
| 35 |
+
locals,
|
| 36 |
+
}: RouterResolutionInput): Promise<RouterResolutionResult> {
|
| 37 |
+
let targetModel = model;
|
| 38 |
+
let candidateModelId: string | undefined;
|
| 39 |
+
let resolvedRoute: string | undefined;
|
| 40 |
+
let runMcp = true;
|
| 41 |
+
|
| 42 |
+
if (!model.isRouter) {
|
| 43 |
+
return { runMcp, targetModel };
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
try {
|
| 47 |
+
const mod = await import("../../models");
|
| 48 |
+
const allModels = mod.models as ProcessedModel[];
|
| 49 |
+
|
| 50 |
+
if (hasImageInput) {
|
| 51 |
+
const multimodalCandidate = allModels?.find(
|
| 52 |
+
(candidate) => !candidate.isRouter && candidate.multimodal
|
| 53 |
+
);
|
| 54 |
+
if (multimodalCandidate) {
|
| 55 |
+
targetModel = multimodalCandidate;
|
| 56 |
+
candidateModelId = multimodalCandidate.id ?? multimodalCandidate.name;
|
| 57 |
+
resolvedRoute = "multimodal";
|
| 58 |
+
} else {
|
| 59 |
+
runMcp = false;
|
| 60 |
+
}
|
| 61 |
+
} else {
|
| 62 |
+
// If tools are enabled and at least one MCP server is active, prefer a tools-capable model
|
| 63 |
+
const toolsEnabled = isRouterToolsBypassEnabled();
|
| 64 |
+
const hasToolsActive = hasActiveToolsSelection(locals);
|
| 65 |
+
|
| 66 |
+
if (toolsEnabled && hasToolsActive) {
|
| 67 |
+
const found = pickToolsCapableModel(allModels);
|
| 68 |
+
if (found) {
|
| 69 |
+
targetModel = found;
|
| 70 |
+
candidateModelId = found.id ?? found.name;
|
| 71 |
+
resolvedRoute = ROUTER_TOOLS_ROUTE;
|
| 72 |
+
// Continue; runMcp remains true
|
| 73 |
+
return { runMcp, targetModel, candidateModelId, resolvedRoute };
|
| 74 |
+
}
|
| 75 |
+
// No tools-capable model found; fall back to normal Arch routing below
|
| 76 |
+
}
|
| 77 |
+
const routes = await getRoutes();
|
| 78 |
+
const sanitized = messages.map(stripReasoningFromMessageForRouting);
|
| 79 |
+
const { routeName } = await archSelectRoute(sanitized, conversationId, locals);
|
| 80 |
+
resolvedRoute = routeName;
|
| 81 |
+
const fallbackModel = config.LLM_ROUTER_FALLBACK_MODEL || model.id;
|
| 82 |
+
const { candidates } = resolveRouteModels(routeName, routes, fallbackModel);
|
| 83 |
+
const primaryCandidateId = candidates[0];
|
| 84 |
+
if (!primaryCandidateId || primaryCandidateId === fallbackModel) {
|
| 85 |
+
runMcp = false;
|
| 86 |
+
} else {
|
| 87 |
+
const found = allModels?.find(
|
| 88 |
+
(candidate) =>
|
| 89 |
+
candidate.id === primaryCandidateId || candidate.name === primaryCandidateId
|
| 90 |
+
);
|
| 91 |
+
if (found) {
|
| 92 |
+
targetModel = found;
|
| 93 |
+
candidateModelId = primaryCandidateId;
|
| 94 |
+
} else {
|
| 95 |
+
runMcp = false;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
} catch (error) {
|
| 100 |
+
logger.warn({ err: String(error) }, "[mcp] routing preflight failed");
|
| 101 |
+
runMcp = false;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
return { runMcp, targetModel, candidateModelId, resolvedRoute };
|
| 105 |
+
}
|
|
@@ -0,0 +1,554 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
+
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
|
| 3 |
+
import type { EndpointMessage } from "../../endpoints/endpoints";
|
| 4 |
+
import { getMcpServers } from "$lib/server/mcp/registry";
|
| 5 |
+
import { isValidUrl } from "$lib/server/urlSafety";
|
| 6 |
+
import { resetMcpToolsCache } from "$lib/server/mcp/tools";
|
| 7 |
+
import { getOpenAiToolsForMcp } from "$lib/server/mcp/tools";
|
| 8 |
+
import type {
|
| 9 |
+
ChatCompletionChunk,
|
| 10 |
+
ChatCompletionCreateParamsStreaming,
|
| 11 |
+
ChatCompletionMessageParam,
|
| 12 |
+
ChatCompletionContentPart,
|
| 13 |
+
ChatCompletionMessageToolCall,
|
| 14 |
+
} from "openai/resources/chat/completions";
|
| 15 |
+
import type { Stream } from "openai/streaming";
|
| 16 |
+
import { buildToolPreprompt } from "../utils/toolPrompt";
|
| 17 |
+
import { resolveRouterTarget } from "./routerResolution";
|
| 18 |
+
import { executeToolCalls, type NormalizedToolCall } from "./toolInvocation";
|
| 19 |
+
import { drainPool } from "$lib/server/mcp/clientPool";
|
| 20 |
+
import { logger } from "../../logger";
|
| 21 |
+
import type { TextGenerationContext } from "../types";
|
| 22 |
+
import { hasAuthHeader, isStrictHfMcpLogin, hasNonEmptyToken } from "$lib/server/mcp/hf";
|
| 23 |
+
|
| 24 |
+
export type RunMcpFlowContext = Pick<
|
| 25 |
+
TextGenerationContext,
|
| 26 |
+
"model" | "conv" | "assistant" | "forceMultimodal" | "forceTools" | "locals"
|
| 27 |
+
> & { messages: EndpointMessage[] };
|
| 28 |
+
|
| 29 |
+
export async function* runMcpFlow({
|
| 30 |
+
model,
|
| 31 |
+
conv,
|
| 32 |
+
messages,
|
| 33 |
+
assistant,
|
| 34 |
+
forceMultimodal,
|
| 35 |
+
forceTools,
|
| 36 |
+
locals,
|
| 37 |
+
preprompt,
|
| 38 |
+
abortSignal,
|
| 39 |
+
}: RunMcpFlowContext & { preprompt?: string; abortSignal?: AbortSignal }): AsyncGenerator<
|
| 40 |
+
MessageUpdate,
|
| 41 |
+
boolean,
|
| 42 |
+
undefined
|
| 43 |
+
> {
|
| 44 |
+
// Start from env-configured servers
|
| 45 |
+
let servers = getMcpServers();
|
| 46 |
+
|
| 47 |
+
// Merge in request-provided custom servers (if any)
|
| 48 |
+
try {
|
| 49 |
+
const reqMcp = (
|
| 50 |
+
locals as unknown as {
|
| 51 |
+
mcp?: {
|
| 52 |
+
selectedServers?: Array<{ name: string; url: string; headers?: Record<string, string> }>;
|
| 53 |
+
selectedServerNames?: string[];
|
| 54 |
+
};
|
| 55 |
+
}
|
| 56 |
+
)?.mcp;
|
| 57 |
+
const custom = Array.isArray(reqMcp?.selectedServers) ? reqMcp?.selectedServers : [];
|
| 58 |
+
if (custom.length > 0) {
|
| 59 |
+
// Invalidate cached tool list when the set of servers changes at request-time
|
| 60 |
+
resetMcpToolsCache();
|
| 61 |
+
// Deduplicate by server name (request takes precedence)
|
| 62 |
+
const byName = new Map<
|
| 63 |
+
string,
|
| 64 |
+
{ name: string; url: string; headers?: Record<string, string> }
|
| 65 |
+
>();
|
| 66 |
+
for (const s of servers) byName.set(s.name, s);
|
| 67 |
+
for (const s of custom) byName.set(s.name, s);
|
| 68 |
+
servers = [...byName.values()];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// If the client specified a selection by name, filter to those
|
| 72 |
+
const names = Array.isArray(reqMcp?.selectedServerNames)
|
| 73 |
+
? reqMcp?.selectedServerNames
|
| 74 |
+
: undefined;
|
| 75 |
+
if (Array.isArray(names)) {
|
| 76 |
+
servers = servers.filter((s) => names.includes(s.name));
|
| 77 |
+
}
|
| 78 |
+
} catch {
|
| 79 |
+
// ignore selection merge errors and proceed with env servers
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Enforce server-side safety (public HTTPS only, no private ranges)
|
| 83 |
+
servers = servers.filter((s) => {
|
| 84 |
+
try {
|
| 85 |
+
return isValidUrl(s.url);
|
| 86 |
+
} catch {
|
| 87 |
+
return false;
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
if (servers.length === 0) {
|
| 91 |
+
logger.warn("[mcp] all selected servers rejected by URL safety guard");
|
| 92 |
+
return false;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Optionally attach the logged-in user's HF token to the official HF MCP server only.
|
| 96 |
+
// Never override an explicit Authorization header, and require token to look like an HF token.
|
| 97 |
+
try {
|
| 98 |
+
const shouldForward = config.MCP_FORWARD_HF_USER_TOKEN === "true";
|
| 99 |
+
const userToken =
|
| 100 |
+
(locals as unknown as { hfAccessToken?: string } | undefined)?.hfAccessToken ??
|
| 101 |
+
(locals as unknown as { token?: string } | undefined)?.token;
|
| 102 |
+
|
| 103 |
+
if (shouldForward && hasNonEmptyToken(userToken)) {
|
| 104 |
+
servers = servers.map((s) => {
|
| 105 |
+
try {
|
| 106 |
+
if (isStrictHfMcpLogin(s.url) && !hasAuthHeader(s.headers)) {
|
| 107 |
+
return {
|
| 108 |
+
...s,
|
| 109 |
+
headers: { ...(s.headers ?? {}), Authorization: `Bearer ${userToken}` },
|
| 110 |
+
};
|
| 111 |
+
}
|
| 112 |
+
} catch {
|
| 113 |
+
// ignore URL parse errors and leave server unchanged
|
| 114 |
+
}
|
| 115 |
+
return s;
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
} catch {
|
| 119 |
+
// best-effort overlay; continue if anything goes wrong
|
| 120 |
+
}
|
| 121 |
+
logger.debug({ count: servers.length }, "[mcp] servers configured");
|
| 122 |
+
if (servers.length === 0) {
|
| 123 |
+
return false;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Gate MCP flow based on model tool support (aggregated) with user override
|
| 127 |
+
try {
|
| 128 |
+
const supportsTools = Boolean((model as unknown as { supportsTools?: boolean }).supportsTools);
|
| 129 |
+
const toolsEnabled = Boolean(forceTools) || supportsTools;
|
| 130 |
+
if (!toolsEnabled) {
|
| 131 |
+
logger.debug({ model: model.id }, "[mcp] tools disabled for model; skipping MCP flow");
|
| 132 |
+
return false;
|
| 133 |
+
}
|
| 134 |
+
} catch {
|
| 135 |
+
// If anything goes wrong reading the flag, proceed (previous behavior)
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const hasImageInput = messages.some((msg) =>
|
| 139 |
+
(msg.files ?? []).some(
|
| 140 |
+
(file) => typeof file?.mime === "string" && file.mime.startsWith("image/")
|
| 141 |
+
)
|
| 142 |
+
);
|
| 143 |
+
|
| 144 |
+
const { runMcp, targetModel, candidateModelId, resolvedRoute } = await resolveRouterTarget({
|
| 145 |
+
model,
|
| 146 |
+
messages,
|
| 147 |
+
conversationId: conv._id.toString(),
|
| 148 |
+
hasImageInput,
|
| 149 |
+
locals,
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
if (!runMcp) {
|
| 153 |
+
logger.debug("[mcp] runMcp=false (router did not select tools path)");
|
| 154 |
+
return false;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
const { tools: oaTools, mapping } = await getOpenAiToolsForMcp(servers, { signal: abortSignal });
|
| 158 |
+
logger.debug({ tools: oaTools.length }, "[mcp] openai tool defs built");
|
| 159 |
+
if (oaTools.length === 0) {
|
| 160 |
+
return false;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
try {
|
| 164 |
+
const { OpenAI } = await import("openai");
|
| 165 |
+
|
| 166 |
+
// Capture provider header (x-inference-provider) from the upstream OpenAI-compatible server.
|
| 167 |
+
let providerHeader: string | undefined;
|
| 168 |
+
const captureProviderFetch = async (
|
| 169 |
+
input: RequestInfo | URL,
|
| 170 |
+
init?: RequestInit
|
| 171 |
+
): Promise<Response> => {
|
| 172 |
+
const res = await fetch(input, init);
|
| 173 |
+
const p = res.headers.get("x-inference-provider");
|
| 174 |
+
if (p && !providerHeader) providerHeader = p;
|
| 175 |
+
return res;
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
const openai = new OpenAI({
|
| 179 |
+
apiKey: config.OPENAI_API_KEY || config.HF_TOKEN || "sk-",
|
| 180 |
+
baseURL: config.OPENAI_BASE_URL,
|
| 181 |
+
fetch: captureProviderFetch,
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
const mmEnabled = (forceMultimodal ?? false) || targetModel.multimodal;
|
| 185 |
+
logger.debug({ model: targetModel.id ?? targetModel.name, mmEnabled }, "[mcp] target model");
|
| 186 |
+
const toOpenAiMessage = (msg: EndpointMessage): ChatCompletionMessageParam => {
|
| 187 |
+
if (msg.from === "user" && mmEnabled) {
|
| 188 |
+
const parts: ChatCompletionContentPart[] = [{ type: "text", text: msg.content }];
|
| 189 |
+
for (const file of msg.files ?? []) {
|
| 190 |
+
if (typeof file?.mime === "string" && file.mime.startsWith("image/")) {
|
| 191 |
+
const rawValue = file.value as unknown;
|
| 192 |
+
let encoded: string;
|
| 193 |
+
if (typeof rawValue === "string") {
|
| 194 |
+
encoded = rawValue;
|
| 195 |
+
} else if (rawValue instanceof Uint8Array) {
|
| 196 |
+
encoded = Buffer.from(rawValue).toString("base64");
|
| 197 |
+
} else if (rawValue instanceof ArrayBuffer) {
|
| 198 |
+
encoded = Buffer.from(rawValue).toString("base64");
|
| 199 |
+
} else {
|
| 200 |
+
encoded = String(rawValue ?? "");
|
| 201 |
+
}
|
| 202 |
+
const url = encoded.startsWith("data:")
|
| 203 |
+
? encoded
|
| 204 |
+
: `data:${file.mime};base64,${encoded}`;
|
| 205 |
+
parts.push({ type: "image_url", image_url: { url, detail: "auto" } });
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
return { role: msg.from, content: parts };
|
| 209 |
+
}
|
| 210 |
+
return { role: msg.from, content: msg.content };
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
let messagesOpenAI: ChatCompletionMessageParam[] = messages.map(toOpenAiMessage);
|
| 214 |
+
const toolPreprompt = buildToolPreprompt(oaTools);
|
| 215 |
+
const prepromptPieces: string[] = [];
|
| 216 |
+
if (toolPreprompt.trim().length > 0) {
|
| 217 |
+
prepromptPieces.push(toolPreprompt);
|
| 218 |
+
}
|
| 219 |
+
if (typeof preprompt === "string" && preprompt.trim().length > 0) {
|
| 220 |
+
prepromptPieces.push(preprompt);
|
| 221 |
+
}
|
| 222 |
+
const mergedPreprompt = prepromptPieces.join("\n\n");
|
| 223 |
+
const hasSystemMessage = messagesOpenAI.length > 0 && messagesOpenAI[0]?.role === "system";
|
| 224 |
+
if (hasSystemMessage) {
|
| 225 |
+
if (mergedPreprompt.length > 0) {
|
| 226 |
+
const existing = messagesOpenAI[0].content ?? "";
|
| 227 |
+
const existingText = typeof existing === "string" ? existing : "";
|
| 228 |
+
messagesOpenAI[0].content = mergedPreprompt + (existingText ? "\n\n" + existingText : "");
|
| 229 |
+
}
|
| 230 |
+
} else if (mergedPreprompt.length > 0) {
|
| 231 |
+
messagesOpenAI = [{ role: "system", content: mergedPreprompt }, ...messagesOpenAI];
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Work around servers that reject `system` role
|
| 235 |
+
if (
|
| 236 |
+
typeof config.OPENAI_BASE_URL === "string" &&
|
| 237 |
+
config.OPENAI_BASE_URL.length > 0 &&
|
| 238 |
+
(config.OPENAI_BASE_URL.includes("hf.space") ||
|
| 239 |
+
config.OPENAI_BASE_URL.includes("gradio.app")) &&
|
| 240 |
+
messagesOpenAI[0]?.role === "system"
|
| 241 |
+
) {
|
| 242 |
+
messagesOpenAI[0] = { ...messagesOpenAI[0], role: "user" };
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
const parameters = { ...targetModel.parameters, ...assistant?.generateSettings } as Record<
|
| 246 |
+
string,
|
| 247 |
+
unknown
|
| 248 |
+
>;
|
| 249 |
+
const maxTokens =
|
| 250 |
+
(parameters?.max_tokens as number | undefined) ??
|
| 251 |
+
(parameters?.max_new_tokens as number | undefined) ??
|
| 252 |
+
(parameters?.max_completion_tokens as number | undefined);
|
| 253 |
+
|
| 254 |
+
const stopSequences =
|
| 255 |
+
typeof parameters?.stop === "string"
|
| 256 |
+
? parameters.stop
|
| 257 |
+
: Array.isArray(parameters?.stop)
|
| 258 |
+
? (parameters.stop as string[])
|
| 259 |
+
: undefined;
|
| 260 |
+
|
| 261 |
+
const completionBase: Omit<ChatCompletionCreateParamsStreaming, "messages"> = {
|
| 262 |
+
model: targetModel.id ?? targetModel.name,
|
| 263 |
+
stream: true,
|
| 264 |
+
temperature: typeof parameters?.temperature === "number" ? parameters.temperature : undefined,
|
| 265 |
+
top_p: typeof parameters?.top_p === "number" ? parameters.top_p : undefined,
|
| 266 |
+
frequency_penalty:
|
| 267 |
+
typeof parameters?.frequency_penalty === "number"
|
| 268 |
+
? parameters.frequency_penalty
|
| 269 |
+
: typeof parameters?.repetition_penalty === "number"
|
| 270 |
+
? parameters.repetition_penalty
|
| 271 |
+
: undefined,
|
| 272 |
+
presence_penalty:
|
| 273 |
+
typeof parameters?.presence_penalty === "number" ? parameters.presence_penalty : undefined,
|
| 274 |
+
stop: stopSequences,
|
| 275 |
+
max_tokens: typeof maxTokens === "number" ? maxTokens : undefined,
|
| 276 |
+
tools: oaTools,
|
| 277 |
+
tool_choice: "auto",
|
| 278 |
+
};
|
| 279 |
+
|
| 280 |
+
const toPrimitive = (value: unknown) => {
|
| 281 |
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
| 282 |
+
return value;
|
| 283 |
+
}
|
| 284 |
+
return undefined;
|
| 285 |
+
};
|
| 286 |
+
|
| 287 |
+
const parseArgs = (raw: unknown): Record<string, unknown> => {
|
| 288 |
+
if (typeof raw !== "string" || raw.trim().length === 0) return {};
|
| 289 |
+
try {
|
| 290 |
+
return JSON.parse(raw);
|
| 291 |
+
} catch {
|
| 292 |
+
return {};
|
| 293 |
+
}
|
| 294 |
+
};
|
| 295 |
+
|
| 296 |
+
const processToolOutput = (
|
| 297 |
+
text: string
|
| 298 |
+
): {
|
| 299 |
+
annotated: string;
|
| 300 |
+
sources: { index: number; link: string }[];
|
| 301 |
+
} => ({ annotated: text, sources: [] });
|
| 302 |
+
|
| 303 |
+
let lastAssistantContent = "";
|
| 304 |
+
let streamedContent = false;
|
| 305 |
+
// Track whether we're inside a <think> block when the upstream streams
|
| 306 |
+
// provider-specific reasoning tokens (e.g. `reasoning` or `reasoning_content`).
|
| 307 |
+
let thinkOpen = false;
|
| 308 |
+
|
| 309 |
+
if (resolvedRoute && candidateModelId) {
|
| 310 |
+
yield {
|
| 311 |
+
type: MessageUpdateType.RouterMetadata,
|
| 312 |
+
route: resolvedRoute,
|
| 313 |
+
model: candidateModelId,
|
| 314 |
+
};
|
| 315 |
+
logger.debug(
|
| 316 |
+
{ route: resolvedRoute, model: candidateModelId },
|
| 317 |
+
"[mcp] router metadata emitted"
|
| 318 |
+
);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
for (let loop = 0; loop < 10; loop += 1) {
|
| 322 |
+
lastAssistantContent = "";
|
| 323 |
+
streamedContent = false;
|
| 324 |
+
|
| 325 |
+
const completionRequest: ChatCompletionCreateParamsStreaming = {
|
| 326 |
+
...completionBase,
|
| 327 |
+
messages: messagesOpenAI,
|
| 328 |
+
};
|
| 329 |
+
|
| 330 |
+
const completionStream: Stream<ChatCompletionChunk> = await openai.chat.completions.create(
|
| 331 |
+
completionRequest,
|
| 332 |
+
{
|
| 333 |
+
signal: abortSignal,
|
| 334 |
+
headers: {
|
| 335 |
+
"ChatUI-Conversation-ID": conv._id.toString(),
|
| 336 |
+
"X-use-cache": "false",
|
| 337 |
+
},
|
| 338 |
+
}
|
| 339 |
+
);
|
| 340 |
+
|
| 341 |
+
// If provider header was exposed, notify UI so it can render "via {provider}".
|
| 342 |
+
if (providerHeader) {
|
| 343 |
+
yield {
|
| 344 |
+
type: MessageUpdateType.RouterMetadata,
|
| 345 |
+
route: "",
|
| 346 |
+
model: "",
|
| 347 |
+
provider: providerHeader as unknown as import("@huggingface/inference").InferenceProvider,
|
| 348 |
+
};
|
| 349 |
+
logger.debug({ provider: providerHeader }, "[mcp] provider metadata emitted");
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
const toolCallState: Record<number, { id?: string; name?: string; arguments: string }> = {};
|
| 353 |
+
let sawToolCall = false;
|
| 354 |
+
let tokenCount = 0;
|
| 355 |
+
for await (const chunk of completionStream) {
|
| 356 |
+
const choice = chunk.choices?.[0];
|
| 357 |
+
const delta = choice?.delta;
|
| 358 |
+
if (!delta) continue;
|
| 359 |
+
|
| 360 |
+
const chunkToolCalls = delta.tool_calls ?? [];
|
| 361 |
+
if (chunkToolCalls.length > 0) {
|
| 362 |
+
sawToolCall = true;
|
| 363 |
+
for (const call of chunkToolCalls) {
|
| 364 |
+
const toolCall = call as unknown as {
|
| 365 |
+
index?: number;
|
| 366 |
+
id?: string;
|
| 367 |
+
function?: { name?: string; arguments?: string };
|
| 368 |
+
};
|
| 369 |
+
const index = toolCall.index ?? 0;
|
| 370 |
+
const current = toolCallState[index] ?? { arguments: "" };
|
| 371 |
+
if (toolCall.id) current.id = toolCall.id;
|
| 372 |
+
if (toolCall.function?.name) current.name = toolCall.function.name;
|
| 373 |
+
if (toolCall.function?.arguments) current.arguments += toolCall.function.arguments;
|
| 374 |
+
toolCallState[index] = current;
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
const deltaContent = (() => {
|
| 379 |
+
if (typeof delta.content === "string") return delta.content;
|
| 380 |
+
const maybeParts = delta.content as unknown;
|
| 381 |
+
if (Array.isArray(maybeParts)) {
|
| 382 |
+
return maybeParts
|
| 383 |
+
.map((part) =>
|
| 384 |
+
typeof part === "object" &&
|
| 385 |
+
part !== null &&
|
| 386 |
+
"text" in part &&
|
| 387 |
+
typeof (part as Record<string, unknown>).text === "string"
|
| 388 |
+
? String((part as Record<string, unknown>).text)
|
| 389 |
+
: ""
|
| 390 |
+
)
|
| 391 |
+
.join("");
|
| 392 |
+
}
|
| 393 |
+
return "";
|
| 394 |
+
})();
|
| 395 |
+
|
| 396 |
+
// Provider-dependent reasoning fields (e.g., `reasoning` or `reasoning_content`).
|
| 397 |
+
const deltaReasoning: string =
|
| 398 |
+
typeof (delta as unknown as Record<string, unknown>)?.reasoning === "string"
|
| 399 |
+
? ((delta as unknown as { reasoning?: string }).reasoning as string)
|
| 400 |
+
: typeof (delta as unknown as Record<string, unknown>)?.reasoning_content === "string"
|
| 401 |
+
? ((delta as unknown as { reasoning_content?: string }).reasoning_content as string)
|
| 402 |
+
: "";
|
| 403 |
+
|
| 404 |
+
// Merge reasoning + content into a single combined token stream, mirroring
|
| 405 |
+
// the OpenAI adapter so the UI can auto-detect <think> blocks.
|
| 406 |
+
let combined = "";
|
| 407 |
+
if (deltaReasoning && deltaReasoning.length > 0) {
|
| 408 |
+
if (!thinkOpen) {
|
| 409 |
+
combined += "<think>" + deltaReasoning;
|
| 410 |
+
thinkOpen = true;
|
| 411 |
+
} else {
|
| 412 |
+
combined += deltaReasoning;
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
if (deltaContent && deltaContent.length > 0) {
|
| 417 |
+
if (thinkOpen) {
|
| 418 |
+
combined += "</think>" + deltaContent;
|
| 419 |
+
thinkOpen = false;
|
| 420 |
+
} else {
|
| 421 |
+
combined += deltaContent;
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
if (combined.length > 0) {
|
| 426 |
+
lastAssistantContent += combined;
|
| 427 |
+
if (!sawToolCall) {
|
| 428 |
+
streamedContent = true;
|
| 429 |
+
yield { type: MessageUpdateType.Stream, token: combined };
|
| 430 |
+
tokenCount += combined.length;
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
logger.debug(
|
| 435 |
+
{ sawToolCalls: Object.keys(toolCallState).length > 0, tokens: tokenCount, loop },
|
| 436 |
+
"[mcp] completion stream closed"
|
| 437 |
+
);
|
| 438 |
+
|
| 439 |
+
if (Object.keys(toolCallState).length > 0) {
|
| 440 |
+
// If any streamed call is missing id, perform a quick non-stream retry to recover full tool_calls with ids
|
| 441 |
+
const missingId = Object.values(toolCallState).some((c) => c?.name && !c?.id);
|
| 442 |
+
let calls: NormalizedToolCall[];
|
| 443 |
+
if (missingId) {
|
| 444 |
+
const nonStream = await openai.chat.completions.create(
|
| 445 |
+
{ ...completionBase, messages: messagesOpenAI, stream: false },
|
| 446 |
+
{ signal: abortSignal }
|
| 447 |
+
);
|
| 448 |
+
const tc = nonStream.choices?.[0]?.message?.tool_calls ?? [];
|
| 449 |
+
calls = tc.map((t) => ({
|
| 450 |
+
id: t.id,
|
| 451 |
+
name: t.function?.name ?? "",
|
| 452 |
+
arguments: t.function?.arguments ?? "",
|
| 453 |
+
}));
|
| 454 |
+
} else {
|
| 455 |
+
calls = Object.values(toolCallState)
|
| 456 |
+
.map((c) => (c?.id && c?.name ? c : undefined))
|
| 457 |
+
.filter(Boolean)
|
| 458 |
+
.map((c) => ({
|
| 459 |
+
id: c?.id ?? "",
|
| 460 |
+
name: c?.name ?? "",
|
| 461 |
+
arguments: c?.arguments ?? "",
|
| 462 |
+
})) as NormalizedToolCall[];
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// Include the assistant message with tool_calls so the next round
|
| 466 |
+
// sees both the calls and their outputs, matching MCP branch behavior.
|
| 467 |
+
const toolCalls: ChatCompletionMessageToolCall[] = calls.map((call) => ({
|
| 468 |
+
id: call.id,
|
| 469 |
+
type: "function",
|
| 470 |
+
function: { name: call.name, arguments: call.arguments },
|
| 471 |
+
}));
|
| 472 |
+
|
| 473 |
+
// Avoid sending <think> content back to the model alongside tool_calls
|
| 474 |
+
// to prevent confusing follow-up reasoning. Strip any think blocks.
|
| 475 |
+
const assistantContentForToolMsg = lastAssistantContent.replace(
|
| 476 |
+
/<think>[\s\S]*?(?:<\/think>|$)/g,
|
| 477 |
+
""
|
| 478 |
+
);
|
| 479 |
+
const assistantToolMessage: ChatCompletionMessageParam = {
|
| 480 |
+
role: "assistant",
|
| 481 |
+
content: assistantContentForToolMsg,
|
| 482 |
+
tool_calls: toolCalls,
|
| 483 |
+
};
|
| 484 |
+
|
| 485 |
+
const exec = executeToolCalls({
|
| 486 |
+
calls,
|
| 487 |
+
mapping,
|
| 488 |
+
servers,
|
| 489 |
+
parseArgs,
|
| 490 |
+
toPrimitive,
|
| 491 |
+
processToolOutput,
|
| 492 |
+
abortSignal,
|
| 493 |
+
});
|
| 494 |
+
let toolMsgCount = 0;
|
| 495 |
+
let toolRunCount = 0;
|
| 496 |
+
for await (const event of exec) {
|
| 497 |
+
if (event.type === "update") {
|
| 498 |
+
yield event.update;
|
| 499 |
+
} else {
|
| 500 |
+
messagesOpenAI = [
|
| 501 |
+
...messagesOpenAI,
|
| 502 |
+
assistantToolMessage,
|
| 503 |
+
...(event.summary.toolMessages ?? []),
|
| 504 |
+
];
|
| 505 |
+
toolMsgCount = event.summary.toolMessages?.length ?? 0;
|
| 506 |
+
toolRunCount = event.summary.toolRuns?.length ?? 0;
|
| 507 |
+
logger.debug(
|
| 508 |
+
{ toolMsgCount, toolRunCount },
|
| 509 |
+
"[mcp] tools executed; continuing loop for follow-up completion"
|
| 510 |
+
);
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
// Continue loop: next iteration will use tool messages to get the final content
|
| 514 |
+
continue;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
// No tool calls: finalize and return
|
| 518 |
+
// If a <think> block is still open, close it for the final output
|
| 519 |
+
if (thinkOpen) {
|
| 520 |
+
lastAssistantContent += "</think>";
|
| 521 |
+
thinkOpen = false;
|
| 522 |
+
}
|
| 523 |
+
if (!streamedContent && lastAssistantContent.trim().length > 0) {
|
| 524 |
+
yield { type: MessageUpdateType.Stream, token: lastAssistantContent };
|
| 525 |
+
}
|
| 526 |
+
yield {
|
| 527 |
+
type: MessageUpdateType.FinalAnswer,
|
| 528 |
+
text: lastAssistantContent,
|
| 529 |
+
interrupted: false,
|
| 530 |
+
};
|
| 531 |
+
logger.debug({ length: lastAssistantContent.length, loop }, "[mcp] final answer emitted");
|
| 532 |
+
return true;
|
| 533 |
+
}
|
| 534 |
+
logger.warn("[mcp] exceeded tool-followup loops; falling back");
|
| 535 |
+
} catch (err) {
|
| 536 |
+
const msg = String(err ?? "");
|
| 537 |
+
const isAbort =
|
| 538 |
+
(abortSignal && abortSignal.aborted) ||
|
| 539 |
+
msg.includes("AbortError") ||
|
| 540 |
+
msg.includes("APIUserAbortError") ||
|
| 541 |
+
msg.includes("Request was aborted");
|
| 542 |
+
if (isAbort) {
|
| 543 |
+
// Expected on user stop; keep logs quiet and do not treat as error
|
| 544 |
+
logger.debug("[mcp] aborted by user");
|
| 545 |
+
return false;
|
| 546 |
+
}
|
| 547 |
+
logger.warn({ err: msg }, "[mcp] flow failed, falling back to default endpoint");
|
| 548 |
+
} finally {
|
| 549 |
+
// ensure MCP clients are closed after the turn
|
| 550 |
+
await drainPool();
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
return false;
|
| 554 |
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomUUID } from "crypto";
|
| 2 |
+
import { logger } from "../../logger";
|
| 3 |
+
import type { MessageUpdate } from "$lib/types/MessageUpdate";
|
| 4 |
+
import { MessageToolUpdateType, MessageUpdateType } from "$lib/types/MessageUpdate";
|
| 5 |
+
import { ToolResultStatus } from "$lib/types/Tool";
|
| 6 |
+
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
|
| 7 |
+
import type { McpToolMapping } from "$lib/server/mcp/tools";
|
| 8 |
+
import type { McpServerConfig } from "$lib/server/mcp/httpClient";
|
| 9 |
+
import { callMcpTool, type McpToolTextResponse } from "$lib/server/mcp/httpClient";
|
| 10 |
+
import { getClient } from "$lib/server/mcp/clientPool";
|
| 11 |
+
import type { Client } from "@modelcontextprotocol/sdk/client";
|
| 12 |
+
|
| 13 |
+
export type Primitive = string | number | boolean;
|
| 14 |
+
|
| 15 |
+
export type ToolRun = {
|
| 16 |
+
name: string;
|
| 17 |
+
parameters: Record<string, Primitive>;
|
| 18 |
+
output: string;
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export interface NormalizedToolCall {
|
| 22 |
+
id: string;
|
| 23 |
+
name: string;
|
| 24 |
+
arguments: string;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export interface ExecuteToolCallsParams {
|
| 28 |
+
calls: NormalizedToolCall[];
|
| 29 |
+
mapping: Record<string, McpToolMapping>;
|
| 30 |
+
servers: McpServerConfig[];
|
| 31 |
+
parseArgs: (raw: unknown) => Record<string, unknown>;
|
| 32 |
+
toPrimitive: (value: unknown) => Primitive | undefined;
|
| 33 |
+
processToolOutput: (text: string) => {
|
| 34 |
+
annotated: string;
|
| 35 |
+
sources: { index: number; link: string }[];
|
| 36 |
+
};
|
| 37 |
+
abortSignal?: AbortSignal;
|
| 38 |
+
toolTimeoutMs?: number;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface ToolCallExecutionResult {
|
| 42 |
+
toolMessages: ChatCompletionMessageParam[];
|
| 43 |
+
toolRuns: ToolRun[];
|
| 44 |
+
finalAnswer?: { text: string; interrupted: boolean };
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export type ToolExecutionEvent =
|
| 48 |
+
| { type: "update"; update: MessageUpdate }
|
| 49 |
+
| { type: "complete"; summary: ToolCallExecutionResult };
|
| 50 |
+
|
| 51 |
+
const serverMap = (servers: McpServerConfig[]): Map<string, McpServerConfig> => {
|
| 52 |
+
const map = new Map<string, McpServerConfig>();
|
| 53 |
+
for (const server of servers) {
|
| 54 |
+
if (server?.name) {
|
| 55 |
+
map.set(server.name, server);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
return map;
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
export async function* executeToolCalls({
|
| 62 |
+
calls,
|
| 63 |
+
mapping,
|
| 64 |
+
servers,
|
| 65 |
+
parseArgs,
|
| 66 |
+
toPrimitive,
|
| 67 |
+
processToolOutput,
|
| 68 |
+
abortSignal,
|
| 69 |
+
toolTimeoutMs = 30_000,
|
| 70 |
+
}: ExecuteToolCallsParams): AsyncGenerator<ToolExecutionEvent, void, undefined> {
|
| 71 |
+
const toolMessages: ChatCompletionMessageParam[] = [];
|
| 72 |
+
const toolRuns: ToolRun[] = [];
|
| 73 |
+
const serverLookup = serverMap(servers);
|
| 74 |
+
// Pre-emit call + ETA updates and prepare tasks
|
| 75 |
+
type TaskResult = {
|
| 76 |
+
index: number;
|
| 77 |
+
output?: string;
|
| 78 |
+
structured?: unknown;
|
| 79 |
+
blocks?: unknown[];
|
| 80 |
+
error?: string;
|
| 81 |
+
uuid: string;
|
| 82 |
+
paramsClean: Record<string, Primitive>;
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const prepared = calls.map((call) => {
|
| 86 |
+
const argsObj = parseArgs(call.arguments);
|
| 87 |
+
const paramsClean: Record<string, Primitive> = {};
|
| 88 |
+
for (const [k, v] of Object.entries(argsObj ?? {})) {
|
| 89 |
+
const prim = toPrimitive(v);
|
| 90 |
+
if (prim !== undefined) paramsClean[k] = prim;
|
| 91 |
+
}
|
| 92 |
+
return { call, argsObj, paramsClean, uuid: randomUUID() };
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
for (const p of prepared) {
|
| 96 |
+
yield {
|
| 97 |
+
type: "update",
|
| 98 |
+
update: {
|
| 99 |
+
type: MessageUpdateType.Tool,
|
| 100 |
+
subtype: MessageToolUpdateType.Call,
|
| 101 |
+
uuid: p.uuid,
|
| 102 |
+
call: { name: p.call.name, parameters: p.paramsClean },
|
| 103 |
+
},
|
| 104 |
+
};
|
| 105 |
+
yield {
|
| 106 |
+
type: "update",
|
| 107 |
+
update: {
|
| 108 |
+
type: MessageUpdateType.Tool,
|
| 109 |
+
subtype: MessageToolUpdateType.ETA,
|
| 110 |
+
uuid: p.uuid,
|
| 111 |
+
eta: 10,
|
| 112 |
+
},
|
| 113 |
+
};
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Preload clients per distinct server used in this batch
|
| 117 |
+
const distinctServerNames = Array.from(
|
| 118 |
+
new Set(prepared.map((p) => mapping[p.call.name]?.server).filter(Boolean) as string[])
|
| 119 |
+
);
|
| 120 |
+
const clientMap = new Map<string, Client>();
|
| 121 |
+
await Promise.all(
|
| 122 |
+
distinctServerNames.map(async (name) => {
|
| 123 |
+
const cfg = serverLookup.get(name);
|
| 124 |
+
if (!cfg) return;
|
| 125 |
+
try {
|
| 126 |
+
const client = await getClient(cfg, abortSignal);
|
| 127 |
+
clientMap.set(name, client);
|
| 128 |
+
} catch (e) {
|
| 129 |
+
logger.warn({ server: name, err: String(e) }, "[mcp] failed to connect client");
|
| 130 |
+
}
|
| 131 |
+
})
|
| 132 |
+
);
|
| 133 |
+
|
| 134 |
+
// Async queue to stream results in finish order
|
| 135 |
+
function createQueue<T>() {
|
| 136 |
+
const items: T[] = [];
|
| 137 |
+
const waiters: Array<(v: IteratorResult<T>) => void> = [];
|
| 138 |
+
let closed = false;
|
| 139 |
+
return {
|
| 140 |
+
push(item: T) {
|
| 141 |
+
const waiter = waiters.shift();
|
| 142 |
+
if (waiter) waiter({ value: item, done: false });
|
| 143 |
+
else items.push(item);
|
| 144 |
+
},
|
| 145 |
+
close() {
|
| 146 |
+
closed = true;
|
| 147 |
+
let waiter: ((v: IteratorResult<T>) => void) | undefined;
|
| 148 |
+
while ((waiter = waiters.shift())) {
|
| 149 |
+
waiter({ value: undefined as unknown as T, done: true });
|
| 150 |
+
}
|
| 151 |
+
},
|
| 152 |
+
async *iterator() {
|
| 153 |
+
for (;;) {
|
| 154 |
+
if (items.length) {
|
| 155 |
+
const first = items.shift();
|
| 156 |
+
if (first !== undefined) yield first as T;
|
| 157 |
+
continue;
|
| 158 |
+
}
|
| 159 |
+
if (closed) return;
|
| 160 |
+
const value: IteratorResult<T> = await new Promise((res) => waiters.push(res));
|
| 161 |
+
if (value.done) return;
|
| 162 |
+
yield value.value as T;
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
};
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
const q = createQueue<TaskResult>();
|
| 169 |
+
|
| 170 |
+
const tasks = prepared.map(async (p, index) => {
|
| 171 |
+
const mappingEntry = mapping[p.call.name];
|
| 172 |
+
if (!mappingEntry) {
|
| 173 |
+
q.push({
|
| 174 |
+
index,
|
| 175 |
+
error: `Unknown MCP function: ${p.call.name}`,
|
| 176 |
+
uuid: p.uuid,
|
| 177 |
+
paramsClean: p.paramsClean,
|
| 178 |
+
});
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
const serverCfg = serverLookup.get(mappingEntry.server);
|
| 182 |
+
if (!serverCfg) {
|
| 183 |
+
q.push({
|
| 184 |
+
index,
|
| 185 |
+
error: `Unknown MCP server: ${mappingEntry.server}`,
|
| 186 |
+
uuid: p.uuid,
|
| 187 |
+
paramsClean: p.paramsClean,
|
| 188 |
+
});
|
| 189 |
+
return;
|
| 190 |
+
}
|
| 191 |
+
const client = clientMap.get(mappingEntry.server);
|
| 192 |
+
try {
|
| 193 |
+
logger.debug(
|
| 194 |
+
{ server: mappingEntry.server, tool: mappingEntry.tool, parameters: p.paramsClean },
|
| 195 |
+
"[mcp] invoking tool"
|
| 196 |
+
);
|
| 197 |
+
const toolResponse: McpToolTextResponse = await callMcpTool(
|
| 198 |
+
serverCfg,
|
| 199 |
+
mappingEntry.tool,
|
| 200 |
+
p.argsObj,
|
| 201 |
+
{
|
| 202 |
+
client,
|
| 203 |
+
signal: abortSignal,
|
| 204 |
+
timeoutMs: toolTimeoutMs,
|
| 205 |
+
}
|
| 206 |
+
);
|
| 207 |
+
const { annotated } = processToolOutput(toolResponse.text ?? "");
|
| 208 |
+
logger.debug(
|
| 209 |
+
{ server: mappingEntry.server, tool: mappingEntry.tool },
|
| 210 |
+
"[mcp] tool call completed"
|
| 211 |
+
);
|
| 212 |
+
q.push({
|
| 213 |
+
index,
|
| 214 |
+
output: annotated,
|
| 215 |
+
structured: toolResponse.structured,
|
| 216 |
+
blocks: toolResponse.content,
|
| 217 |
+
uuid: p.uuid,
|
| 218 |
+
paramsClean: p.paramsClean,
|
| 219 |
+
});
|
| 220 |
+
} catch (err) {
|
| 221 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 222 |
+
logger.warn(
|
| 223 |
+
{ server: mappingEntry.server, tool: mappingEntry.tool, err: message },
|
| 224 |
+
"[mcp] tool call failed"
|
| 225 |
+
);
|
| 226 |
+
q.push({ index, error: message, uuid: p.uuid, paramsClean: p.paramsClean });
|
| 227 |
+
}
|
| 228 |
+
});
|
| 229 |
+
|
| 230 |
+
// kick off and stream as they finish
|
| 231 |
+
Promise.allSettled(tasks).then(() => q.close());
|
| 232 |
+
|
| 233 |
+
const results: TaskResult[] = [];
|
| 234 |
+
for await (const r of q.iterator()) {
|
| 235 |
+
results.push(r);
|
| 236 |
+
if (r.error) {
|
| 237 |
+
yield {
|
| 238 |
+
type: "update",
|
| 239 |
+
update: {
|
| 240 |
+
type: MessageUpdateType.Tool,
|
| 241 |
+
subtype: MessageToolUpdateType.Error,
|
| 242 |
+
uuid: r.uuid,
|
| 243 |
+
message: r.error,
|
| 244 |
+
},
|
| 245 |
+
};
|
| 246 |
+
} else {
|
| 247 |
+
yield {
|
| 248 |
+
type: "update",
|
| 249 |
+
update: {
|
| 250 |
+
type: MessageUpdateType.Tool,
|
| 251 |
+
subtype: MessageToolUpdateType.Result,
|
| 252 |
+
uuid: r.uuid,
|
| 253 |
+
result: {
|
| 254 |
+
status: ToolResultStatus.Success,
|
| 255 |
+
call: { name: prepared[r.index].call.name, parameters: r.paramsClean },
|
| 256 |
+
outputs: [
|
| 257 |
+
{
|
| 258 |
+
text: r.output ?? "",
|
| 259 |
+
structured: r.structured,
|
| 260 |
+
content: r.blocks,
|
| 261 |
+
} as unknown as Record<string, unknown>,
|
| 262 |
+
],
|
| 263 |
+
display: true,
|
| 264 |
+
},
|
| 265 |
+
},
|
| 266 |
+
};
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// Collate outputs in original call order
|
| 271 |
+
results.sort((a, b) => a.index - b.index);
|
| 272 |
+
for (const r of results) {
|
| 273 |
+
const name = prepared[r.index].call.name;
|
| 274 |
+
const id = prepared[r.index].call.id;
|
| 275 |
+
if (!r.error) {
|
| 276 |
+
const output = r.output ?? "";
|
| 277 |
+
toolRuns.push({ name, parameters: r.paramsClean, output });
|
| 278 |
+
// For the LLM follow-up call, we keep only the textual output
|
| 279 |
+
toolMessages.push({ role: "tool", tool_call_id: id, content: output });
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
yield { type: "complete", summary: { toolMessages, toolRuns } };
|
| 284 |
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
|
| 2 |
+
import { MessageUpdateType } from "$lib/types/MessageUpdate";
|
| 3 |
+
|
| 4 |
+
export async function generateSummaryOfReasoning(
|
| 5 |
+
reasoning: string,
|
| 6 |
+
modelId: string | undefined,
|
| 7 |
+
locals: App.Locals | undefined
|
| 8 |
+
): Promise<string> {
|
| 9 |
+
const prompt = `Summarize concisely the following reasoning for the user. Keep it short (one short paragraph).\n\n${reasoning}`;
|
| 10 |
+
const summary = await (async () => {
|
| 11 |
+
const it = generateFromDefaultEndpoint({
|
| 12 |
+
messages: [{ from: "user", content: prompt }],
|
| 13 |
+
modelId,
|
| 14 |
+
locals,
|
| 15 |
+
});
|
| 16 |
+
let out = "";
|
| 17 |
+
for await (const update of it) {
|
| 18 |
+
if (update.type === MessageUpdateType.Stream) out += update.token;
|
| 19 |
+
}
|
| 20 |
+
return out;
|
| 21 |
+
})();
|
| 22 |
+
return summary.trim();
|
| 23 |
+
}
|
|
@@ -15,6 +15,8 @@ export interface TextGenerationContext {
|
|
| 15 |
username?: string;
|
| 16 |
/** Force-enable multimodal handling for endpoints that support it */
|
| 17 |
forceMultimodal?: boolean;
|
|
|
|
|
|
|
| 18 |
locals: App.Locals | undefined;
|
| 19 |
abortController: AbortController;
|
| 20 |
}
|
|
|
|
| 15 |
username?: string;
|
| 16 |
/** Force-enable multimodal handling for endpoints that support it */
|
| 17 |
forceMultimodal?: boolean;
|
| 18 |
+
/** Force-enable tool calling even if model does not advertise support */
|
| 19 |
+
forceTools?: boolean;
|
| 20 |
locals: App.Locals | undefined;
|
| 21 |
abortController: AbortController;
|
| 22 |
}
|
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { EndpointMessage } from "../../endpoints/endpoints";
|
| 2 |
+
|
| 3 |
+
const ROUTER_REASONING_REGEX = /<think>[\s\S]*?(?:<\/think>|$)/g;
|
| 4 |
+
|
| 5 |
+
export function stripReasoningBlocks(text: string): string {
|
| 6 |
+
const stripped = text.replace(ROUTER_REASONING_REGEX, "");
|
| 7 |
+
return stripped === text ? text : stripped.trim();
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function stripReasoningFromMessageForRouting(message: EndpointMessage): EndpointMessage {
|
| 11 |
+
const clone = { ...message } as EndpointMessage & { reasoning?: string };
|
| 12 |
+
if ("reasoning" in clone) {
|
| 13 |
+
delete clone.reasoning;
|
| 14 |
+
}
|
| 15 |
+
const content =
|
| 16 |
+
typeof message.content === "string" ? stripReasoningBlocks(message.content) : message.content;
|
| 17 |
+
return {
|
| 18 |
+
...clone,
|
| 19 |
+
content,
|
| 20 |
+
};
|
| 21 |
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { OpenAiTool } from "$lib/server/mcp/tools";
|
| 2 |
+
|
| 3 |
+
export function buildToolPreprompt(tools: OpenAiTool[]): string {
|
| 4 |
+
if (!Array.isArray(tools) || tools.length === 0) return "";
|
| 5 |
+
const names = tools
|
| 6 |
+
.map((t) => (t?.function?.name ? String(t.function.name) : ""))
|
| 7 |
+
.filter((s) => s.length > 0);
|
| 8 |
+
if (names.length === 0) return "";
|
| 9 |
+
const currentDate = new Date().toLocaleDateString("en-US", {
|
| 10 |
+
year: "numeric",
|
| 11 |
+
month: "long",
|
| 12 |
+
day: "numeric",
|
| 13 |
+
});
|
| 14 |
+
return `You can use the following tools if helpful: ${names.join(", ")}. Today's date: ${currentDate}. If a tool generates an image, you can inline it directly: .`;
|
| 15 |
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared server-side URL safety helper (exact behavior preserved)
|
| 2 |
+
export function isValidUrl(urlString: string): boolean {
|
| 3 |
+
try {
|
| 4 |
+
const url = new URL(urlString);
|
| 5 |
+
// Only allow HTTPS protocol
|
| 6 |
+
if (url.protocol !== "https:") {
|
| 7 |
+
return false;
|
| 8 |
+
}
|
| 9 |
+
// Prevent localhost/private IPs (basic check)
|
| 10 |
+
const hostname = url.hostname.toLowerCase();
|
| 11 |
+
if (
|
| 12 |
+
hostname === "localhost" ||
|
| 13 |
+
hostname.startsWith("127.") ||
|
| 14 |
+
hostname.startsWith("192.168.") ||
|
| 15 |
+
hostname.startsWith("172.16.") ||
|
| 16 |
+
hostname === "[::1]" ||
|
| 17 |
+
hostname === "0.0.0.0"
|
| 18 |
+
) {
|
| 19 |
+
return false;
|
| 20 |
+
}
|
| 21 |
+
return true;
|
| 22 |
+
} catch {
|
| 23 |
+
return false;
|
| 24 |
+
}
|
| 25 |
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* MCP Servers Store
|
| 3 |
+
* Manages base (env-configured) and custom (user-added) MCP servers
|
| 4 |
+
* Stores custom servers and selection state in browser localStorage
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { writable, derived } from "svelte/store";
|
| 8 |
+
import { base } from "$app/paths";
|
| 9 |
+
import { browser } from "$app/environment";
|
| 10 |
+
import type { MCPServer, ServerStatus, MCPTool } from "$lib/types/Tool";
|
| 11 |
+
|
| 12 |
+
const STORAGE_KEYS = {
|
| 13 |
+
CUSTOM_SERVERS: "mcp:custom-servers",
|
| 14 |
+
SELECTED_IDS: "mcp:selected-ids",
|
| 15 |
+
} as const;
|
| 16 |
+
|
| 17 |
+
// Load custom servers from localStorage
|
| 18 |
+
function loadCustomServers(): MCPServer[] {
|
| 19 |
+
if (!browser) return [];
|
| 20 |
+
|
| 21 |
+
try {
|
| 22 |
+
const json = localStorage.getItem(STORAGE_KEYS.CUSTOM_SERVERS);
|
| 23 |
+
return json ? JSON.parse(json) : [];
|
| 24 |
+
} catch (error) {
|
| 25 |
+
console.error("Failed to load custom MCP servers from localStorage:", error);
|
| 26 |
+
return [];
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Load selected server IDs from localStorage
|
| 31 |
+
function loadSelectedIds(): Set<string> {
|
| 32 |
+
if (!browser) return new Set();
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
const json = localStorage.getItem(STORAGE_KEYS.SELECTED_IDS);
|
| 36 |
+
const ids: string[] = json ? JSON.parse(json) : [];
|
| 37 |
+
return new Set(ids);
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error("Failed to load selected MCP server IDs from localStorage:", error);
|
| 40 |
+
return new Set();
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Save custom servers to localStorage
|
| 45 |
+
function saveCustomServers(servers: MCPServer[]) {
|
| 46 |
+
if (!browser) return;
|
| 47 |
+
|
| 48 |
+
try {
|
| 49 |
+
localStorage.setItem(STORAGE_KEYS.CUSTOM_SERVERS, JSON.stringify(servers));
|
| 50 |
+
} catch (error) {
|
| 51 |
+
console.error("Failed to save custom MCP servers to localStorage:", error);
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Save selected IDs to localStorage
|
| 56 |
+
function saveSelectedIds(ids: Set<string>) {
|
| 57 |
+
if (!browser) return;
|
| 58 |
+
|
| 59 |
+
try {
|
| 60 |
+
localStorage.setItem(STORAGE_KEYS.SELECTED_IDS, JSON.stringify([...ids]));
|
| 61 |
+
} catch (error) {
|
| 62 |
+
console.error("Failed to save selected MCP server IDs to localStorage:", error);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Store for all servers (base + custom)
|
| 67 |
+
export const allMcpServers = writable<MCPServer[]>([]);
|
| 68 |
+
|
| 69 |
+
// Store for selected server IDs
|
| 70 |
+
export const selectedServerIds = writable<Set<string>>(loadSelectedIds());
|
| 71 |
+
|
| 72 |
+
// Auto-persist selected IDs when they change
|
| 73 |
+
if (browser) {
|
| 74 |
+
selectedServerIds.subscribe((ids) => {
|
| 75 |
+
saveSelectedIds(ids);
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Derived store: only enabled servers
|
| 80 |
+
export const enabledServers = derived([allMcpServers, selectedServerIds], ([$all, $selected]) =>
|
| 81 |
+
$all.filter((s) => $selected.has(s.id))
|
| 82 |
+
);
|
| 83 |
+
|
| 84 |
+
// Derived store: count of enabled servers
|
| 85 |
+
export const enabledServersCount = derived(enabledServers, ($enabled) => $enabled.length);
|
| 86 |
+
|
| 87 |
+
// Note: Authorization overlay (with user's HF token) for the Hugging Face MCP host
|
| 88 |
+
// is applied server-side when enabled via MCP_FORWARD_HF_USER_TOKEN.
|
| 89 |
+
|
| 90 |
+
/**
|
| 91 |
+
* Refresh base servers from API and merge with custom servers
|
| 92 |
+
*/
|
| 93 |
+
export async function refreshMcpServers() {
|
| 94 |
+
try {
|
| 95 |
+
const response = await fetch(`${base}/api/mcp/servers`);
|
| 96 |
+
if (!response.ok) {
|
| 97 |
+
throw new Error(`Failed to fetch base servers: ${response.statusText}`);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
const baseServers: MCPServer[] = await response.json();
|
| 101 |
+
const customServers = loadCustomServers();
|
| 102 |
+
|
| 103 |
+
// Merge base and custom servers
|
| 104 |
+
const merged = [...baseServers, ...customServers];
|
| 105 |
+
allMcpServers.set(merged);
|
| 106 |
+
|
| 107 |
+
// Prune selected IDs that no longer correspond to existing servers
|
| 108 |
+
const validIds = new Set(merged.map((s) => s.id));
|
| 109 |
+
selectedServerIds.update(($ids) => {
|
| 110 |
+
const filtered = new Set([...$ids].filter((id) => validIds.has(id)));
|
| 111 |
+
return filtered;
|
| 112 |
+
});
|
| 113 |
+
} catch (error) {
|
| 114 |
+
console.error("Failed to refresh MCP servers:", error);
|
| 115 |
+
// On error, just use custom servers
|
| 116 |
+
allMcpServers.set(loadCustomServers());
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/**
|
| 121 |
+
* Toggle a server on/off
|
| 122 |
+
*/
|
| 123 |
+
export function toggleServer(id: string) {
|
| 124 |
+
selectedServerIds.update(($ids) => {
|
| 125 |
+
const newSet = new Set($ids);
|
| 126 |
+
if (newSet.has(id)) {
|
| 127 |
+
newSet.delete(id);
|
| 128 |
+
} else {
|
| 129 |
+
newSet.add(id);
|
| 130 |
+
}
|
| 131 |
+
return newSet;
|
| 132 |
+
});
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/**
|
| 136 |
+
* Add a custom MCP server
|
| 137 |
+
*/
|
| 138 |
+
export function addCustomServer(server: Omit<MCPServer, "id" | "type" | "status">): string {
|
| 139 |
+
const newServer: MCPServer = {
|
| 140 |
+
...server,
|
| 141 |
+
id: crypto.randomUUID(),
|
| 142 |
+
type: "custom",
|
| 143 |
+
status: "disconnected",
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const customServers = loadCustomServers();
|
| 147 |
+
customServers.push(newServer);
|
| 148 |
+
saveCustomServers(customServers);
|
| 149 |
+
|
| 150 |
+
// Refresh all servers to include the new one
|
| 151 |
+
refreshMcpServers();
|
| 152 |
+
|
| 153 |
+
return newServer.id;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/**
|
| 157 |
+
* Update an existing custom server
|
| 158 |
+
*/
|
| 159 |
+
export function updateCustomServer(id: string, updates: Partial<MCPServer>) {
|
| 160 |
+
const customServers = loadCustomServers();
|
| 161 |
+
const index = customServers.findIndex((s) => s.id === id);
|
| 162 |
+
|
| 163 |
+
if (index !== -1) {
|
| 164 |
+
customServers[index] = { ...customServers[index], ...updates };
|
| 165 |
+
saveCustomServers(customServers);
|
| 166 |
+
refreshMcpServers();
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/**
|
| 171 |
+
* Delete a custom server
|
| 172 |
+
*/
|
| 173 |
+
export function deleteCustomServer(id: string) {
|
| 174 |
+
const customServers = loadCustomServers();
|
| 175 |
+
const filtered = customServers.filter((s) => s.id !== id);
|
| 176 |
+
saveCustomServers(filtered);
|
| 177 |
+
|
| 178 |
+
// Also remove from selected IDs
|
| 179 |
+
selectedServerIds.update(($ids) => {
|
| 180 |
+
const newSet = new Set($ids);
|
| 181 |
+
newSet.delete(id);
|
| 182 |
+
return newSet;
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
refreshMcpServers();
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Update server status (from health check)
|
| 190 |
+
*/
|
| 191 |
+
export function updateServerStatus(
|
| 192 |
+
id: string,
|
| 193 |
+
status: ServerStatus,
|
| 194 |
+
errorMessage?: string,
|
| 195 |
+
tools?: MCPTool[],
|
| 196 |
+
authRequired?: boolean
|
| 197 |
+
) {
|
| 198 |
+
allMcpServers.update(($servers) =>
|
| 199 |
+
$servers.map((s) =>
|
| 200 |
+
s.id === id
|
| 201 |
+
? {
|
| 202 |
+
...s,
|
| 203 |
+
status,
|
| 204 |
+
errorMessage,
|
| 205 |
+
tools,
|
| 206 |
+
authRequired,
|
| 207 |
+
}
|
| 208 |
+
: s
|
| 209 |
+
)
|
| 210 |
+
);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* Run health check on a server
|
| 215 |
+
*/
|
| 216 |
+
export async function healthCheckServer(
|
| 217 |
+
server: MCPServer
|
| 218 |
+
): Promise<{ ready: boolean; tools?: MCPTool[]; error?: string }> {
|
| 219 |
+
try {
|
| 220 |
+
updateServerStatus(server.id, "connecting");
|
| 221 |
+
|
| 222 |
+
const response = await fetch(`${base}/api/mcp/health`, {
|
| 223 |
+
method: "POST",
|
| 224 |
+
headers: { "Content-Type": "application/json" },
|
| 225 |
+
body: JSON.stringify({ url: server.url, headers: server.headers }),
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
const result = await response.json();
|
| 229 |
+
|
| 230 |
+
if (result.ready && result.tools) {
|
| 231 |
+
updateServerStatus(server.id, "connected", undefined, result.tools, false);
|
| 232 |
+
return { ready: true, tools: result.tools };
|
| 233 |
+
} else {
|
| 234 |
+
updateServerStatus(server.id, "error", result.error, undefined, Boolean(result.authRequired));
|
| 235 |
+
return { ready: false, error: result.error };
|
| 236 |
+
}
|
| 237 |
+
} catch (error) {
|
| 238 |
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
| 239 |
+
updateServerStatus(server.id, "error", errorMessage);
|
| 240 |
+
return { ready: false, error: errorMessage };
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Initialize on module load
|
| 245 |
+
if (browser) {
|
| 246 |
+
refreshMcpServers();
|
| 247 |
+
}
|
|
@@ -12,6 +12,7 @@ type SettingsStore = {
|
|
| 12 |
activeModel: string;
|
| 13 |
customPrompts: Record<string, string>;
|
| 14 |
multimodalOverrides: Record<string, boolean>;
|
|
|
|
| 15 |
recentlySaved: boolean;
|
| 16 |
disableStream: boolean;
|
| 17 |
directPaste: boolean;
|
|
|
|
| 12 |
activeModel: string;
|
| 13 |
customPrompts: Record<string, string>;
|
| 14 |
multimodalOverrides: Record<string, boolean>;
|
| 15 |
+
toolsOverrides: Record<string, boolean>;
|
| 16 |
recentlySaved: boolean;
|
| 17 |
disableStream: boolean;
|
| 18 |
directPaste: boolean;
|
|
@@ -9,6 +9,8 @@ export type Message = Partial<Timestamps> & {
|
|
| 9 |
content: string;
|
| 10 |
updates?: MessageUpdate[];
|
| 11 |
|
|
|
|
|
|
|
| 12 |
score?: -1 | 0 | 1;
|
| 13 |
/**
|
| 14 |
* Either contains the base64 encoded image data
|
|
|
|
| 9 |
content: string;
|
| 10 |
updates?: MessageUpdate[];
|
| 11 |
|
| 12 |
+
// Optional server or client-side reasoning content (<think> blocks)
|
| 13 |
+
reasoning?: string;
|
| 14 |
score?: -1 | 0 | 1;
|
| 15 |
/**
|
| 16 |
* Either contains the base64 encoded image data
|
|
@@ -1,8 +1,10 @@
|
|
| 1 |
import type { InferenceProvider } from "@huggingface/inference";
|
|
|
|
| 2 |
|
| 3 |
export type MessageUpdate =
|
| 4 |
| MessageStatusUpdate
|
| 5 |
| MessageTitleUpdate
|
|
|
|
| 6 |
| MessageStreamUpdate
|
| 7 |
| MessageFileUpdate
|
| 8 |
| MessageFinalAnswerUpdate
|
|
@@ -12,6 +14,7 @@ export type MessageUpdate =
|
|
| 12 |
export enum MessageUpdateType {
|
| 13 |
Status = "status",
|
| 14 |
Title = "title",
|
|
|
|
| 15 |
Stream = "stream",
|
| 16 |
File = "file",
|
| 17 |
FinalAnswer = "finalAnswer",
|
|
@@ -43,6 +46,43 @@ export interface MessageStreamUpdate {
|
|
| 43 |
token: string;
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
export enum MessageReasoningUpdateType {
|
| 47 |
Stream = "stream",
|
| 48 |
Status = "status",
|
|
|
|
| 1 |
import type { InferenceProvider } from "@huggingface/inference";
|
| 2 |
+
import type { ToolCall, ToolResult } from "$lib/types/Tool";
|
| 3 |
|
| 4 |
export type MessageUpdate =
|
| 5 |
| MessageStatusUpdate
|
| 6 |
| MessageTitleUpdate
|
| 7 |
+
| MessageToolUpdate
|
| 8 |
| MessageStreamUpdate
|
| 9 |
| MessageFileUpdate
|
| 10 |
| MessageFinalAnswerUpdate
|
|
|
|
| 14 |
export enum MessageUpdateType {
|
| 15 |
Status = "status",
|
| 16 |
Title = "title",
|
| 17 |
+
Tool = "tool",
|
| 18 |
Stream = "stream",
|
| 19 |
File = "file",
|
| 20 |
FinalAnswer = "finalAnswer",
|
|
|
|
| 46 |
token: string;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
// Tool updates (for MCP and function calling)
|
| 50 |
+
export enum MessageToolUpdateType {
|
| 51 |
+
Call = "call",
|
| 52 |
+
Result = "result",
|
| 53 |
+
Error = "error",
|
| 54 |
+
ETA = "eta",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
interface MessageToolUpdateBase<TSubtype extends MessageToolUpdateType> {
|
| 58 |
+
type: MessageUpdateType.Tool;
|
| 59 |
+
subtype: TSubtype;
|
| 60 |
+
uuid: string;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export interface MessageToolCallUpdate extends MessageToolUpdateBase<MessageToolUpdateType.Call> {
|
| 64 |
+
call: ToolCall;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface MessageToolResultUpdate
|
| 68 |
+
extends MessageToolUpdateBase<MessageToolUpdateType.Result> {
|
| 69 |
+
result: ToolResult;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export interface MessageToolErrorUpdate extends MessageToolUpdateBase<MessageToolUpdateType.Error> {
|
| 73 |
+
message: string;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export interface MessageToolEtaUpdate extends MessageToolUpdateBase<MessageToolUpdateType.ETA> {
|
| 77 |
+
eta: number;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export type MessageToolUpdate =
|
| 81 |
+
| MessageToolCallUpdate
|
| 82 |
+
| MessageToolResultUpdate
|
| 83 |
+
| MessageToolErrorUpdate
|
| 84 |
+
| MessageToolEtaUpdate;
|
| 85 |
+
|
| 86 |
export enum MessageReasoningUpdateType {
|
| 87 |
Stream = "stream",
|
| 88 |
Status = "status",
|
|
@@ -21,6 +21,12 @@ export interface Settings extends Timestamps {
|
|
| 21 |
*/
|
| 22 |
multimodalOverrides?: Record<string, boolean>;
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
/**
|
| 25 |
* Per-model toggle to hide Omni prompt suggestions shown near the composer.
|
| 26 |
* When set to `true`, prompt examples for that model are suppressed.
|
|
@@ -38,6 +44,7 @@ export const DEFAULT_SETTINGS = {
|
|
| 38 |
activeModel: defaultModel.id,
|
| 39 |
customPrompts: {},
|
| 40 |
multimodalOverrides: {},
|
|
|
|
| 41 |
hidePromptExamples: {},
|
| 42 |
disableStream: false,
|
| 43 |
directPaste: false,
|
|
|
|
| 21 |
*/
|
| 22 |
multimodalOverrides?: Record<string, boolean>;
|
| 23 |
|
| 24 |
+
/**
|
| 25 |
+
* Per‑model overrides to enable tool calling (OpenAI tools/function calling)
|
| 26 |
+
* even when not advertised by the provider list. Only `true` is meaningful.
|
| 27 |
+
*/
|
| 28 |
+
toolsOverrides?: Record<string, boolean>;
|
| 29 |
+
|
| 30 |
/**
|
| 31 |
* Per-model toggle to hide Omni prompt suggestions shown near the composer.
|
| 32 |
* When set to `true`, prompt examples for that model are suppressed.
|
|
|
|
| 44 |
activeModel: defaultModel.id,
|
| 45 |
customPrompts: {},
|
| 46 |
multimodalOverrides: {},
|
| 47 |
+
toolsOverrides: {},
|
| 48 |
hidePromptExamples: {},
|
| 49 |
disableStream: false,
|
| 50 |
directPaste: false,
|
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export enum ToolResultStatus {
|
| 2 |
+
Success = "success",
|
| 3 |
+
Error = "error",
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export interface ToolCall {
|
| 7 |
+
name: string;
|
| 8 |
+
parameters: Record<string, string | number | boolean>;
|
| 9 |
+
toolId?: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface ToolResultSuccess {
|
| 13 |
+
status: ToolResultStatus.Success;
|
| 14 |
+
call: ToolCall;
|
| 15 |
+
outputs: Record<string, unknown>[];
|
| 16 |
+
display?: boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface ToolResultError {
|
| 20 |
+
status: ToolResultStatus.Error;
|
| 21 |
+
call: ToolCall;
|
| 22 |
+
message: string;
|
| 23 |
+
display?: boolean;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export type ToolResult = ToolResultSuccess | ToolResultError;
|
| 27 |
+
|
| 28 |
+
export interface ToolFront {
|
| 29 |
+
_id: string;
|
| 30 |
+
name: string;
|
| 31 |
+
displayName?: string;
|
| 32 |
+
description?: string;
|
| 33 |
+
color?: string;
|
| 34 |
+
icon?: string;
|
| 35 |
+
type?: "config" | "community";
|
| 36 |
+
isOnByDefault?: boolean;
|
| 37 |
+
isLocked?: boolean;
|
| 38 |
+
mimeTypes?: string[];
|
| 39 |
+
timeToUseMS?: number;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// MCP Server types
|
| 43 |
+
export interface KeyValuePair {
|
| 44 |
+
key: string;
|
| 45 |
+
value: string;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export type ServerStatus = "connected" | "connecting" | "disconnected" | "error";
|
| 49 |
+
|
| 50 |
+
export interface MCPTool {
|
| 51 |
+
name: string;
|
| 52 |
+
description?: string;
|
| 53 |
+
inputSchema?: unknown;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export interface MCPServer {
|
| 57 |
+
id: string;
|
| 58 |
+
name: string;
|
| 59 |
+
url: string;
|
| 60 |
+
type: "base" | "custom";
|
| 61 |
+
headers?: KeyValuePair[];
|
| 62 |
+
env?: KeyValuePair[];
|
| 63 |
+
status?: ServerStatus;
|
| 64 |
+
isLocked?: boolean;
|
| 65 |
+
tools?: MCPTool[];
|
| 66 |
+
errorMessage?: string;
|
| 67 |
+
// Indicates server reports or appears to require OAuth or other auth
|
| 68 |
+
authRequired?: boolean;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export interface MCPServerApi {
|
| 72 |
+
url: string;
|
| 73 |
+
headers?: KeyValuePair[];
|
| 74 |
+
}
|