From 0aa927f629cc6ca9c23791db1a5214fe834c4014 Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 30 Apr 2025 14:43:55 -0400 Subject: [PATCH 1/6] Add auto-injest functionality to pre-load MCP servers --- app/api/mcp-config/route.ts | 15 ++++++++++ app/providers.tsx | 2 ++ components/auto-inject-mcp.tsx | 55 ++++++++++++++++++++++++++++++++++ mcp.json | 4 +++ 4 files changed, 76 insertions(+) create mode 100644 app/api/mcp-config/route.ts create mode 100644 components/auto-inject-mcp.tsx create mode 100644 mcp.json diff --git a/app/api/mcp-config/route.ts b/app/api/mcp-config/route.ts new file mode 100644 index 0000000..ea8f3e3 --- /dev/null +++ b/app/api/mcp-config/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { promises as fs } from "fs"; +import path from "path"; + +export async function GET() { + try { + // Resolve the path to the project root and mcp.json + const filePath = path.resolve(process.cwd(), "mcp.json"); + const fileContents = await fs.readFile(filePath, "utf-8"); + const json = JSON.parse(fileContents); + return NextResponse.json(json); + } catch (error) { + return NextResponse.json({ error: "Could not read mcp.json", details: error instanceof Error ? error.message : error }, { status: 500 }); + } +} diff --git a/app/providers.tsx b/app/providers.tsx index 3a7f0cb..13bcd27 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -8,6 +8,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useLocalStorage } from "@/lib/hooks/use-local-storage"; import { STORAGE_KEYS } from "@/lib/constants"; import { MCPProvider } from "@/lib/context/mcp-context"; +import { AutoInjectMCP } from "@/components/auto-inject-mcp"; // Create a client const queryClient = new QueryClient({ @@ -35,6 +36,7 @@ export function Providers({ children }: { children: ReactNode }) { themes={["light", "dark", "sunset", "black"]} > + {children} diff --git a/components/auto-inject-mcp.tsx b/components/auto-inject-mcp.tsx new file mode 100644 index 0000000..41a85ab --- /dev/null +++ b/components/auto-inject-mcp.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useEffect } from "react"; +import { STORAGE_KEYS } from "@/lib/constants"; +import { useLocalStorage } from "@/lib/hooks/use-local-storage"; +import type { MCPServer } from "@/lib/context/mcp-context"; + +export function AutoInjectMCP() { + const [mcpServers, setMcpServers] = useLocalStorage( + STORAGE_KEYS.MCP_SERVERS, + [] + ); + const [selectedMcpServers, setSelectedMcpServers] = useLocalStorage( + STORAGE_KEYS.SELECTED_MCP_SERVERS, + [] + ); + + useEffect(() => { + async function injectServers() { + try { + const res = await fetch("/api/mcp-config"); + if (!res.ok) throw new Error("Failed to fetch mcp-config"); + const data = await res.json(); + const servers = data.mcpServers || {}; + const newServers: MCPServer[] = []; + const newSelected: string[] = []; + Object.entries(servers).forEach(([id, config]) => { + // Merge id into config if not present + const server: MCPServer = { id, ...config }; + // Add if not already present + if (!mcpServers.some(s => s.id === id)) { + newServers.push(server); + } + // Auto-enable if specified + if (config.autoEnable && !selectedMcpServers.includes(id)) { + newSelected.push(id); + } + }); + if (newServers.length > 0) { + setMcpServers([...mcpServers, ...newServers]); + } + if (newSelected.length > 0) { + setSelectedMcpServers([...selectedMcpServers, ...newSelected]); + } + } catch (e) { + console.error("AutoInjectMCP failed to inject servers:", e); + } + } + injectServers(); + // Only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..b075d8c --- /dev/null +++ b/mcp.json @@ -0,0 +1,4 @@ +{ + "mcpServers": { + } +} \ No newline at end of file From 5bd92ec9afcc7a291073fad134eb8615547e5cca Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 30 Apr 2025 14:52:28 -0400 Subject: [PATCH 2/6] update docs --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index ffc1911..5d80097 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,54 @@ You can use any MCP-compatible server with this application. Here are some examp - [Zapier MCP](https://zapier.com/mcp) - Provides access to Zapier tools - Any MCP server using stdio transport with npx and python3 +## MCP Server Auto-Inject Functionality + +This project supports automatic injection of MCP (Model Context Protocol) servers at runtime using a root-level `mcp.json` file. This allows you to pre-configure which MCP servers are available and which should be enabled by default when the app starts. + +### How It Works +- Place an `mcp.json` file in your project root. +- Define all desired MCP servers under the `mcpServers` object. +- Each server configuration can include: + - `type`: The type of server (e.g., `stdio`, `sse`, `http`). + - `command`: The command to launch the MCP server (e.g., `npx`, `python3`). + - `args`: An array of arguments to pass to the command. For example, `['-y', '@modelcontextprotocol/server-github']` will run `npx -y @modelcontextprotocol/server-github`. + - Use this to specify package names, flags, or script entry points. + - `env`: An object of environment variables to set when launching the server. For example, `{ "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_TOKEN" }` will set the token in the server's environment. + - Use this for API keys, model configuration, or any server-specific environment settings. + - `autoEnable`: If `true`, the server will be enabled automatically at app launch. + +**Best Practices:** +- Use `args` to keep your command line flexible and easy to update without changing the command itself. +- Store sensitive information like API keys in `env` and reference environment variables as needed. +- You can add as many custom environment variables as your MCP server supports. + +The app will read this file at startup and inject all listed servers into the UI. Servers with `autoEnable: true` will be selected for immediate use. + +### Sample mcp.json +```json +{ + "mcpServers": { + "github": { + "type": "stdio", // type of server, e.g., sse or stdio + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-github" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN" + }, + "autoEnable": true // auto-enable at launch + } + } +} +``` + +- You may add multiple servers under `mcpServers`. +- All fields are customizable per server. + +--- + ## License This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. \ No newline at end of file From db5cd6c683b79742f36d33e791baa34599b8a859 Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 30 Apr 2025 14:54:28 -0400 Subject: [PATCH 3/6] streamline docs --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 5d80097..f1a9f2b 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,7 @@ This project supports automatic injection of MCP (Model Context Protocol) server - `type`: The type of server (e.g., `stdio`, `sse`, `http`). - `command`: The command to launch the MCP server (e.g., `npx`, `python3`). - `args`: An array of arguments to pass to the command. For example, `['-y', '@modelcontextprotocol/server-github']` will run `npx -y @modelcontextprotocol/server-github`. - - Use this to specify package names, flags, or script entry points. - `env`: An object of environment variables to set when launching the server. For example, `{ "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_TOKEN" }` will set the token in the server's environment. - - Use this for API keys, model configuration, or any server-specific environment settings. - `autoEnable`: If `true`, the server will be enabled automatically at app launch. **Best Practices:** From 18039c083d23938e08c1698ae57575a3bc4e888d Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 30 Apr 2025 15:20:19 -0400 Subject: [PATCH 4/6] add changeset --- .changeset/README.md | 8 ++++++++ .changeset/config.json | 11 +++++++++++ .changeset/thick-dodos-sneeze.md | 6 ++++++ 3 files changed, 25 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .changeset/thick-dodos-sneeze.md diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..d88011f --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/thick-dodos-sneeze.md b/.changeset/thick-dodos-sneeze.md new file mode 100644 index 0000000..bf75dcd --- /dev/null +++ b/.changeset/thick-dodos-sneeze.md @@ -0,0 +1,6 @@ +--- +"mcp-chat": patch +--- + +Add auto-inject functionality to pre-load MCP servers +- Resolves #9 \ No newline at end of file From 70b40c5270f345484d6d0651c302dc2bbeb3e0d2 Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 30 Apr 2025 16:15:23 -0400 Subject: [PATCH 5/6] correctly process env vars into --- app/api/mcp-config/route.ts | 7 +++++++ components/auto-inject-mcp.tsx | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/api/mcp-config/route.ts b/app/api/mcp-config/route.ts index ea8f3e3..b01f67c 100644 --- a/app/api/mcp-config/route.ts +++ b/app/api/mcp-config/route.ts @@ -8,6 +8,13 @@ export async function GET() { const filePath = path.resolve(process.cwd(), "mcp.json"); const fileContents = await fs.readFile(filePath, "utf-8"); const json = JSON.parse(fileContents); + if (json.mcpServers && typeof json.mcpServers === 'object') { + for (const [id, config] of Object.entries(json.mcpServers)) { + if (config.env && typeof config.env === 'object' && !Array.isArray(config.env)) { + config.env = Object.entries(config.env).map(([key, value]) => ({ key, value })); + } + } + } return NextResponse.json(json); } catch (error) { return NextResponse.json({ error: "Could not read mcp.json", details: error instanceof Error ? error.message : error }, { status: 500 }); diff --git a/components/auto-inject-mcp.tsx b/components/auto-inject-mcp.tsx index 41a85ab..cfca1ac 100644 --- a/components/auto-inject-mcp.tsx +++ b/components/auto-inject-mcp.tsx @@ -25,8 +25,13 @@ export function AutoInjectMCP() { const newServers: MCPServer[] = []; const newSelected: string[] = []; Object.entries(servers).forEach(([id, config]) => { - // Merge id into config if not present - const server: MCPServer = { id, ...config }; + // Normalize env to always be an array of { key, value } + let env = config.env; + if (env && typeof env === "object" && !Array.isArray(env)) { + env = Object.entries(env).map(([key, value]) => ({ key, value })); + } + // Merge id and normalized env into config + const server: MCPServer = { id, ...config, env }; // Add if not already present if (!mcpServers.some(s => s.id === id)) { newServers.push(server); From aa9c68d1a9ea06057976c7fb788ef6fec8da160d Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 30 Apr 2025 16:24:08 -0400 Subject: [PATCH 6/6] fix formatting & install npm run format --- .changeset/thick-dodos-sneeze.md | 5 +- .prettierignore | 4 + .prettierrc | 5 + README.md | 12 +- ai/providers.ts | 92 +- app/actions.ts | 43 +- app/api/chat/route.ts | 104 +- app/api/chats/[id]/route.ts | 45 +- app/api/chats/route.ts | 21 +- app/api/mcp-config/route.ts | 29 +- app/chat/[id]/page.tsx | 24 +- app/globals.css | 212 +- app/layout.tsx | 50 +- app/page.tsx | 2 +- app/providers.tsx | 32 +- components.json | 2 +- components/api-key-manager.tsx | 120 +- components/auto-inject-mcp.tsx | 24 +- components/chat-sidebar.tsx | 969 ++++--- components/chat.tsx | 141 +- components/copy-button.tsx | 14 +- components/deploy-button.tsx | 4 +- components/icons.tsx | 10 +- components/input.tsx | 8 +- components/markdown.tsx | 85 +- components/mcp-server-manager.tsx | 1861 ++++++------ components/message.tsx | 147 +- components/messages.tsx | 15 +- components/model-picker.tsx | 158 +- components/project-overview.tsx | 2 +- components/suggested-prompts.tsx | 22 +- components/textarea.tsx | 21 +- components/theme-provider.tsx | 12 +- components/theme-toggle.tsx | 38 +- components/tool-invocation.tsx | 86 +- components/ui/accordion.tsx | 28 +- components/ui/avatar.tsx | 26 +- components/ui/badge.tsx | 32 +- components/ui/button.tsx | 46 +- components/ui/dialog.tsx | 54 +- components/ui/dropdown-menu.tsx | 82 +- components/ui/input.tsx | 18 +- components/ui/label.tsx | 16 +- components/ui/popover.tsx | 24 +- components/ui/scroll-area.tsx | 30 +- components/ui/select.tsx | 64 +- components/ui/separator.tsx | 18 +- components/ui/sheet.tsx | 72 +- components/ui/sidebar.tsx | 454 +-- components/ui/skeleton.tsx | 10 +- components/ui/sonner.tsx | 22 +- components/ui/text-morph.tsx | 12 +- components/ui/textarea.tsx | 14 +- components/ui/tooltip.tsx | 22 +- drizzle.config.ts | 14 +- drizzle/meta/0000_snapshot.json | 10 +- drizzle/meta/0001_snapshot.json | 22 +- drizzle/meta/0002_snapshot.json | 18 +- drizzle/meta/0003_snapshot.json | 10 +- drizzle/meta/0004_snapshot.json | 10 +- drizzle/meta/0005_snapshot.json | 10 +- drizzle/meta/_journal.json | 2 +- eslint.config.mjs | 16 +- hooks/use-mobile.ts | 24 +- lib/chat-store.ts | 137 +- lib/constants.ts | 8 +- lib/context/mcp-context.tsx | 42 +- lib/db/index.ts | 8 +- lib/db/schema.ts | 32 +- lib/hooks/use-chats.ts | 34 +- lib/hooks/use-copy.ts | 10 +- lib/hooks/use-local-storage.ts | 47 +- lib/hooks/use-scroll-to-bottom.tsx | 15 +- lib/user-id.ts | 8 +- lib/utils.ts | 6 +- mcp.json | 5 +- next.config.ts | 2 +- package.json | 4 +- pnpm-lock.yaml | 4298 +++++++++++++++++++++------- postcss.config.mjs | 2 +- railpack.json | 28 +- 81 files changed, 6415 insertions(+), 3870 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.changeset/thick-dodos-sneeze.md b/.changeset/thick-dodos-sneeze.md index bf75dcd..a1dbd77 100644 --- a/.changeset/thick-dodos-sneeze.md +++ b/.changeset/thick-dodos-sneeze.md @@ -1,6 +1,7 @@ --- -"mcp-chat": patch +'mcp-chat': patch --- Add auto-inject functionality to pre-load MCP servers -- Resolves #9 \ No newline at end of file + +- Resolves #9 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6968f9c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +build +dist +.next diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7d2081a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md index f1a9f2b..853fd9c 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,14 @@ This application supports connecting to Model Context Protocol (MCP) servers to #### SSE Configuration If you select SSE transport: + 1. Enter the server URL (e.g., `https://mcp.example.com/token/sse`) 2. Click "Add Server" #### stdio Configuration If you select stdio transport: + 1. Enter the command to execute (e.g., `npx`) 2. Enter the command arguments (e.g., `-y @modelcontextprotocol/server-google-maps`) - You can enter space-separated arguments or paste a JSON array @@ -64,6 +66,7 @@ You can use any MCP-compatible server with this application. Here are some examp This project supports automatic injection of MCP (Model Context Protocol) servers at runtime using a root-level `mcp.json` file. This allows you to pre-configure which MCP servers are available and which should be enabled by default when the app starts. ### How It Works + - Place an `mcp.json` file in your project root. - Define all desired MCP servers under the `mcpServers` object. - Each server configuration can include: @@ -74,6 +77,7 @@ This project supports automatic injection of MCP (Model Context Protocol) server - `autoEnable`: If `true`, the server will be enabled automatically at app launch. **Best Practices:** + - Use `args` to keep your command line flexible and easy to update without changing the command itself. - Store sensitive information like API keys in `env` and reference environment variables as needed. - You can add as many custom environment variables as your MCP server supports. @@ -81,16 +85,14 @@ This project supports automatic injection of MCP (Model Context Protocol) server The app will read this file at startup and inject all listed servers into the UI. Servers with `autoEnable: true` will be selected for immediate use. ### Sample mcp.json + ```json { "mcpServers": { "github": { "type": "stdio", // type of server, e.g., sse or stdio "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-github" - ], + "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN" }, @@ -107,4 +109,4 @@ The app will read this file at startup and inject all listed servers into the UI ## License -This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. diff --git a/ai/providers.ts b/ai/providers.ts index b8be49c..aa59eba 100644 --- a/ai/providers.ts +++ b/ai/providers.ts @@ -1,13 +1,13 @@ -import { createOpenAI } from "@ai-sdk/openai"; -import { createGroq } from "@ai-sdk/groq"; -import { createAnthropic } from "@ai-sdk/anthropic"; -import { createXai } from "@ai-sdk/xai"; +import { createOpenAI } from '@ai-sdk/openai'; +import { createGroq } from '@ai-sdk/groq'; +import { createAnthropic } from '@ai-sdk/anthropic'; +import { createXai } from '@ai-sdk/xai'; -import { - customProvider, - wrapLanguageModel, - extractReasoningMiddleware -} from "ai"; +import { + customProvider, + wrapLanguageModel, + extractReasoningMiddleware, +} from 'ai'; export interface ModelInfo { provider: string; @@ -27,12 +27,12 @@ const getApiKey = (key: string): string | undefined => { if (process.env[key]) { return process.env[key] || undefined; } - + // Fall back to localStorage if available if (typeof window !== 'undefined') { return window.localStorage.getItem(key) || undefined; } - + return undefined; }; @@ -54,45 +54,47 @@ const xaiClient = createXai({ }); const languageModels = { - "gpt-4.1-mini": openaiClient("gpt-4.1-mini"), - "claude-3-7-sonnet": anthropicClient('claude-3-7-sonnet-20250219'), - "qwen-qwq": wrapLanguageModel( - { - model: groqClient("qwen-qwq-32b"), - middleware - } - ), - "grok-3-mini": xaiClient("grok-3-mini-latest"), + 'gpt-4.1-mini': openaiClient('gpt-4.1-mini'), + 'claude-3-7-sonnet': anthropicClient('claude-3-7-sonnet-20250219'), + 'qwen-qwq': wrapLanguageModel({ + model: groqClient('qwen-qwq-32b'), + middleware, + }), + 'grok-3-mini': xaiClient('grok-3-mini-latest'), }; export const modelDetails: Record = { - "gpt-4.1-mini": { - provider: "OpenAI", - name: "GPT-4.1 Mini", - description: "Compact version of OpenAI's GPT-4.1 with good balance of capabilities, including vision.", - apiVersion: "gpt-4.1-mini", - capabilities: ["Balance", "Creative", "Vision"] + 'gpt-4.1-mini': { + provider: 'OpenAI', + name: 'GPT-4.1 Mini', + description: + "Compact version of OpenAI's GPT-4.1 with good balance of capabilities, including vision.", + apiVersion: 'gpt-4.1-mini', + capabilities: ['Balance', 'Creative', 'Vision'], }, - "claude-3-7-sonnet": { - provider: "Anthropic", - name: "Claude 3.7 Sonnet", - description: "Latest version of Anthropic's Claude 3.7 Sonnet with strong reasoning and coding capabilities.", - apiVersion: "claude-3-7-sonnet-20250219", - capabilities: ["Reasoning", "Efficient", "Agentic"] + 'claude-3-7-sonnet': { + provider: 'Anthropic', + name: 'Claude 3.7 Sonnet', + description: + "Latest version of Anthropic's Claude 3.7 Sonnet with strong reasoning and coding capabilities.", + apiVersion: 'claude-3-7-sonnet-20250219', + capabilities: ['Reasoning', 'Efficient', 'Agentic'], }, - "qwen-qwq": { - provider: "Groq", - name: "Qwen QWQ", - description: "Latest version of Alibaba's Qwen QWQ with strong reasoning and coding capabilities.", - apiVersion: "qwen-qwq", - capabilities: ["Reasoning", "Efficient", "Agentic"] + 'qwen-qwq': { + provider: 'Groq', + name: 'Qwen QWQ', + description: + "Latest version of Alibaba's Qwen QWQ with strong reasoning and coding capabilities.", + apiVersion: 'qwen-qwq', + capabilities: ['Reasoning', 'Efficient', 'Agentic'], }, - "grok-3-mini": { - provider: "XAI", - name: "Grok 3 Mini", - description: "Latest version of XAI's Grok 3 Mini with strong reasoning and coding capabilities.", - apiVersion: "grok-3-mini-latest", - capabilities: ["Reasoning", "Efficient", "Agentic"] + 'grok-3-mini': { + provider: 'XAI', + name: 'Grok 3 Mini', + description: + "Latest version of XAI's Grok 3 Mini with strong reasoning and coding capabilities.", + apiVersion: 'grok-3-mini-latest', + capabilities: ['Reasoning', 'Efficient', 'Agentic'], }, }; @@ -114,4 +116,4 @@ export type modelID = keyof typeof languageModels; export const MODELS = Object.keys(languageModels); -export const defaultModel: modelID = "qwen-qwq"; +export const defaultModel: modelID = 'qwen-qwq'; diff --git a/app/actions.ts b/app/actions.ts index 487be07..c8d1fbb 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -1,49 +1,52 @@ -"use server"; +'use server'; -import { openai } from "@ai-sdk/openai"; -import { generateObject } from "ai"; -import { z } from "zod"; +import { openai } from '@ai-sdk/openai'; +import { generateObject } from 'ai'; +import { z } from 'zod'; // Helper to extract text content from a message regardless of format function getMessageText(message: any): string { // Check if the message has parts (new format) if (message.parts && Array.isArray(message.parts)) { - const textParts = message.parts.filter((p: any) => p.type === 'text' && p.text); + const textParts = message.parts.filter( + (p: any) => p.type === 'text' && p.text, + ); if (textParts.length > 0) { return textParts.map((p: any) => p.text).join('\n'); } } - + // Fallback to content (old format) if (typeof message.content === 'string') { return message.content; } - + // If content is an array (potentially of parts), try to extract text if (Array.isArray(message.content)) { - const textItems = message.content.filter((item: any) => - typeof item === 'string' || (item.type === 'text' && item.text) + const textItems = message.content.filter( + (item: any) => + typeof item === 'string' || (item.type === 'text' && item.text), ); - + if (textItems.length > 0) { - return textItems.map((item: any) => - typeof item === 'string' ? item : item.text - ).join('\n'); + return textItems + .map((item: any) => (typeof item === 'string' ? item : item.text)) + .join('\n'); } } - + return ''; } export async function generateTitle(messages: any[]) { // Convert messages to a format that OpenAI can understand - const normalizedMessages = messages.map(msg => ({ + const normalizedMessages = messages.map((msg) => ({ role: msg.role, - content: getMessageText(msg) + content: getMessageText(msg), })); - + const { object } = await generateObject({ - model: openai("gpt-4.1"), + model: openai('gpt-4.1'), schema: z.object({ title: z.string().min(1).max(100), }), @@ -56,8 +59,8 @@ export async function generateTitle(messages: any[]) { messages: [ ...normalizedMessages, { - role: "user", - content: "Generate a title for the conversation.", + role: 'user', + content: 'Generate a title for the conversation.', }, ], }); diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 09629a4..3a8b4e5 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,5 +1,5 @@ -import { model, type modelID } from "@/ai/providers"; -import { streamText, type UIMessage } from "ai"; +import { model, type modelID } from '@/ai/providers'; +import { streamText, type UIMessage } from 'ai'; import { appendResponseMessages } from 'ai'; import { saveChat, saveMessages, convertToDBMessages } from '@/lib/chat-store'; import { nanoid } from 'nanoid'; @@ -7,9 +7,12 @@ import { db } from '@/lib/db'; import { chats } from '@/lib/db/schema'; import { eq, and } from 'drizzle-orm'; -import { experimental_createMCPClient as createMCPClient, MCPTransport } from 'ai'; +import { + experimental_createMCPClient as createMCPClient, + MCPTransport, +} from 'ai'; import { Experimental_StdioMCPTransport as StdioMCPTransport } from 'ai/mcp-stdio'; -import { spawn } from "child_process"; +import { spawn } from 'child_process'; // Allow streaming responses up to 30 seconds export const maxDuration = 120; @@ -44,10 +47,10 @@ export async function POST(req: Request) { } = await req.json(); if (!userId) { - return new Response( - JSON.stringify({ error: "User ID is required" }), - { status: 400, headers: { "Content-Type": "application/json" } } - ); + return new Response(JSON.stringify({ error: 'User ID is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); } const id = chatId || nanoid(); @@ -58,14 +61,11 @@ export async function POST(req: Request) { if (chatId) { try { const existingChat = await db.query.chats.findFirst({ - where: and( - eq(chats.id, chatId), - eq(chats.userId, userId) - ) + where: and(eq(chats.id, chatId), eq(chats.userId, userId)), }); isNewChat = !existingChat; } catch (error) { - console.error("Error checking for existing chat:", error); + console.error('Error checking for existing chat:', error); // Continue anyway, we'll create the chat in onFinish isNewChat = true; } @@ -82,13 +82,15 @@ export async function POST(req: Request) { for (const mcpServer of mcpServers) { try { // Create appropriate transport based on type - let transport: MCPTransport | { type: 'sse', url: string, headers?: Record }; + let transport: + | MCPTransport + | { type: 'sse'; url: string; headers?: Record }; if (mcpServer.type === 'sse') { // Convert headers array to object for SSE transport const headers: Record = {}; if (mcpServer.headers && mcpServer.headers.length > 0) { - mcpServer.headers.forEach(header => { + mcpServer.headers.forEach((header) => { if (header.key) headers[header.key] = header.value || ''; }); } @@ -96,19 +98,25 @@ export async function POST(req: Request) { transport = { type: 'sse' as const, url: mcpServer.url, - headers: Object.keys(headers).length > 0 ? headers : undefined + headers: Object.keys(headers).length > 0 ? headers : undefined, }; } else if (mcpServer.type === 'stdio') { // For stdio transport, we need command and args - if (!mcpServer.command || !mcpServer.args || mcpServer.args.length === 0) { - console.warn("Skipping stdio MCP server due to missing command or args"); + if ( + !mcpServer.command || + !mcpServer.args || + mcpServer.args.length === 0 + ) { + console.warn( + 'Skipping stdio MCP server due to missing command or args', + ); continue; } // Convert env array to object for stdio transport const env: Record = {}; if (mcpServer.env && mcpServer.env.length > 0) { - mcpServer.env.forEach(envVar => { + mcpServer.env.forEach((envVar) => { if (envVar.key) env[envVar.key] = envVar.value || ''; }); } @@ -125,19 +133,27 @@ export async function POST(req: Request) { // wait for the subprocess to finish await new Promise((resolve) => { subprocess.on('close', resolve); - console.log("installed uv"); + console.log('installed uv'); }); - console.log("Detected uvx pattern, transforming to python3 -m uv run"); + console.log( + 'Detected uvx pattern, transforming to python3 -m uv run', + ); mcpServer.command = 'python3'; // Get the tool name (first argument) const toolName = mcpServer.args[0]; // Replace args with the new pattern - mcpServer.args = ['-m', 'uv', 'run', toolName, ...mcpServer.args.slice(1)]; + mcpServer.args = [ + '-m', + 'uv', + 'run', + toolName, + ...mcpServer.args.slice(1), + ]; } // if python is passed in the command, install the python package mentioned in args after -m with subprocess or use regex to find the package name else if (mcpServer.command.includes('python3')) { const packageName = mcpServer.args[mcpServer.args.indexOf('-m') + 1]; - console.log("installing python package", packageName); + console.log('installing python package', packageName); const subprocess = spawn('pip3', ['install', packageName]); subprocess.on('close', (code: number) => { if (code !== 0) { @@ -147,17 +163,19 @@ export async function POST(req: Request) { // wait for the subprocess to finish await new Promise((resolve) => { subprocess.on('close', resolve); - console.log("installed python package", packageName); + console.log('installed python package', packageName); }); } transport = new StdioMCPTransport({ command: mcpServer.command, args: mcpServer.args, - env: Object.keys(env).length > 0 ? env : undefined + env: Object.keys(env).length > 0 ? env : undefined, }); } else { - console.warn(`Skipping MCP server with unsupported transport type: ${mcpServer.type}`); + console.warn( + `Skipping MCP server with unsupported transport type: ${mcpServer.type}`, + ); continue; } @@ -166,12 +184,15 @@ export async function POST(req: Request) { const mcptools = await mcpClient.tools(); - console.log(`MCP tools from ${mcpServer.type} transport:`, Object.keys(mcptools)); + console.log( + `MCP tools from ${mcpServer.type} transport:`, + Object.keys(mcptools), + ); // Add MCP tools to tools object tools = { ...tools, ...mcptools }; } catch (error) { - console.error("Failed to initialize MCP client:", error); + console.error('Failed to initialize MCP client:', error); // Continue with other servers instead of failing the entire request } } @@ -183,14 +204,17 @@ export async function POST(req: Request) { try { await client.close(); } catch (error) { - console.error("Error closing MCP client:", error); + console.error('Error closing MCP client:', error); } } }); } - console.log("messages", messages); - console.log("parts", messages.map(m => m.parts.map(p => p))); + console.log('messages', messages); + console.log( + 'parts', + messages.map((m) => m.parts.map((p) => p)), + ); // If there was an error setting up MCP clients but we at least have composio tools, continue const result = streamText({ @@ -227,11 +251,11 @@ export async function POST(req: Request) { }, }, anthropic: { - thinking: { - type: 'enabled', - budgetTokens: 12000 + thinking: { + type: 'enabled', + budgetTokens: 12000, }, - } + }, }, onError: (error) => { console.error(JSON.stringify(error, null, 2)); @@ -254,20 +278,20 @@ export async function POST(req: Request) { // for (const client of mcpClients) { // await client.close(); // } - } + }, }); - result.consumeStream() + result.consumeStream(); return result.toDataStreamResponse({ sendReasoning: true, getErrorMessage: (error) => { if (error instanceof Error) { - if (error.message.includes("Rate limit")) { - return "Rate limit exceeded. Please try again later."; + if (error.message.includes('Rate limit')) { + return 'Rate limit exceeded. Please try again later.'; } } console.error(error); - return "An error occurred."; + return 'An error occurred.'; }, }); } diff --git a/app/api/chats/[id]/route.ts b/app/api/chats/[id]/route.ts index c518b3d..ea892ba 100644 --- a/app/api/chats/[id]/route.ts +++ b/app/api/chats/[id]/route.ts @@ -1,5 +1,5 @@ -import { NextResponse } from "next/server"; -import { getChatById, deleteChat } from "@/lib/chat-store"; +import { NextResponse } from 'next/server'; +import { getChatById, deleteChat } from '@/lib/chat-store'; interface Params { params: { @@ -10,27 +10,27 @@ interface Params { export async function GET(request: Request, { params }: Params) { try { const userId = request.headers.get('x-user-id'); - + if (!userId) { - return NextResponse.json({ error: "User ID is required" }, { status: 400 }); + return NextResponse.json( + { error: 'User ID is required' }, + { status: 400 }, + ); } - + const { id } = await params; const chat = await getChatById(id, userId); - + if (!chat) { - return NextResponse.json( - { error: "Chat not found" }, - { status: 404 } - ); + return NextResponse.json({ error: 'Chat not found' }, { status: 404 }); } - + return NextResponse.json(chat); } catch (error) { - console.error("Error fetching chat:", error); + console.error('Error fetching chat:', error); return NextResponse.json( - { error: "Failed to fetch chat" }, - { status: 500 } + { error: 'Failed to fetch chat' }, + { status: 500 }, ); } } @@ -38,19 +38,22 @@ export async function GET(request: Request, { params }: Params) { export async function DELETE(request: Request, { params }: Params) { try { const userId = request.headers.get('x-user-id'); - + if (!userId) { - return NextResponse.json({ error: "User ID is required" }, { status: 400 }); + return NextResponse.json( + { error: 'User ID is required' }, + { status: 400 }, + ); } - + const { id } = await params; await deleteChat(id, userId); return NextResponse.json({ success: true }); } catch (error) { - console.error("Error deleting chat:", error); + console.error('Error deleting chat:', error); return NextResponse.json( - { error: "Failed to delete chat" }, - { status: 500 } + { error: 'Failed to delete chat' }, + { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/app/api/chats/route.ts b/app/api/chats/route.ts index 2f4f6b5..4b70d7c 100644 --- a/app/api/chats/route.ts +++ b/app/api/chats/route.ts @@ -1,21 +1,24 @@ -import { NextResponse } from "next/server"; -import { getChats } from "@/lib/chat-store"; +import { NextResponse } from 'next/server'; +import { getChats } from '@/lib/chat-store'; export async function GET(request: Request) { try { const userId = request.headers.get('x-user-id'); - + if (!userId) { - return NextResponse.json({ error: "User ID is required" }, { status: 400 }); + return NextResponse.json( + { error: 'User ID is required' }, + { status: 400 }, + ); } - + const chats = await getChats(userId); return NextResponse.json(chats); } catch (error) { - console.error("Error fetching chats:", error); + console.error('Error fetching chats:', error); return NextResponse.json( - { error: "Failed to fetch chats" }, - { status: 500 } + { error: 'Failed to fetch chats' }, + { status: 500 }, ); } -} \ No newline at end of file +} diff --git a/app/api/mcp-config/route.ts b/app/api/mcp-config/route.ts index b01f67c..33cff99 100644 --- a/app/api/mcp-config/route.ts +++ b/app/api/mcp-config/route.ts @@ -1,22 +1,35 @@ -import { NextResponse } from "next/server"; -import { promises as fs } from "fs"; -import path from "path"; +import { NextResponse } from 'next/server'; +import { promises as fs } from 'fs'; +import path from 'path'; export async function GET() { try { // Resolve the path to the project root and mcp.json - const filePath = path.resolve(process.cwd(), "mcp.json"); - const fileContents = await fs.readFile(filePath, "utf-8"); + const filePath = path.resolve(process.cwd(), 'mcp.json'); + const fileContents = await fs.readFile(filePath, 'utf-8'); const json = JSON.parse(fileContents); if (json.mcpServers && typeof json.mcpServers === 'object') { for (const [id, config] of Object.entries(json.mcpServers)) { - if (config.env && typeof config.env === 'object' && !Array.isArray(config.env)) { - config.env = Object.entries(config.env).map(([key, value]) => ({ key, value })); + if ( + config.env && + typeof config.env === 'object' && + !Array.isArray(config.env) + ) { + config.env = Object.entries(config.env).map(([key, value]) => ({ + key, + value, + })); } } } return NextResponse.json(json); } catch (error) { - return NextResponse.json({ error: "Could not read mcp.json", details: error instanceof Error ? error.message : error }, { status: 500 }); + return NextResponse.json( + { + error: 'Could not read mcp.json', + details: error instanceof Error ? error.message : error, + }, + { status: 500 }, + ); } } diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 8581be0..41c934f 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -1,10 +1,10 @@ -"use client"; +'use client'; -import Chat from "@/components/chat"; -import { getUserId } from "@/lib/user-id"; -import { useQueryClient } from "@tanstack/react-query"; -import { useParams } from "next/navigation"; -import { useEffect } from "react"; +import Chat from '@/components/chat'; +import { getUserId } from '@/lib/user-id'; +import { useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'next/navigation'; +import { useEffect } from 'react'; export default function ChatPage() { const params = useParams(); @@ -16,7 +16,7 @@ export default function ChatPage() { useEffect(() => { async function prefetchChat() { if (!chatId || !userId) return; - + // Check if data already exists in cache const existingData = queryClient.getQueryData(['chat', chatId, userId]); if (existingData) return; @@ -28,14 +28,14 @@ export default function ChatPage() { try { const response = await fetch(`/api/chats/${chatId}`, { headers: { - 'x-user-id': userId - } + 'x-user-id': userId, + }, }); - + if (!response.ok) { throw new Error('Failed to load chat'); } - + return response.json(); } catch (error) { console.error('Error prefetching chat:', error); @@ -50,4 +50,4 @@ export default function ChatPage() { }, [chatId, userId, queryClient]); return ; -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index 4af5aa7..a7018ba 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @plugin "tailwindcss-animate"; @@ -9,33 +9,33 @@ :root { --background: oklch(0.99 0.01 56.32); --foreground: oklch(0.34 0.01 2.77); - --card: oklch(1.00 0 0); + --card: oklch(1 0 0); --card-foreground: oklch(0.34 0.01 2.77); - --popover: oklch(1.00 0 0); + --popover: oklch(1 0 0); --popover-foreground: oklch(0.34 0.01 2.77); --primary: oklch(0.74 0.16 34.71); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.96 0.02 28.90); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.96 0.02 28.9); --secondary-foreground: oklch(0.56 0.13 32.74); - --muted: oklch(0.97 0.02 39.40); + --muted: oklch(0.97 0.02 39.4); --muted-foreground: oklch(0.49 0.05 26.45); - --accent: oklch(0.83 0.11 58.00); + --accent: oklch(0.83 0.11 58); --accent-foreground: oklch(0.34 0.01 2.77); --destructive: oklch(0.61 0.21 22.24); - --destructive-foreground: oklch(1.00 0 0); + --destructive-foreground: oklch(1 0 0); --border: oklch(0.93 0.04 38.69); --input: oklch(0.93 0.04 38.69); --ring: oklch(0.74 0.16 34.71); --chart-1: oklch(0.74 0.16 34.71); - --chart-2: oklch(0.83 0.11 58.00); + --chart-2: oklch(0.83 0.11 58); --chart-3: oklch(0.88 0.08 54.93); --chart-4: oklch(0.82 0.11 40.89); --chart-5: oklch(0.64 0.13 32.07); - --sidebar: oklch(0.97 0.02 39.40); + --sidebar: oklch(0.97 0.02 39.4); --sidebar-foreground: oklch(0.34 0.01 2.77); --sidebar-primary: oklch(0.74 0.16 34.71); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.83 0.11 58.00); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.83 0.11 58); --sidebar-accent-foreground: oklch(0.34 0.01 2.77); --sidebar-border: oklch(0.93 0.04 38.69); --sidebar-ring: oklch(0.74 0.16 34.71); @@ -45,44 +45,49 @@ --radius: 0.625rem; --shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); --shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); - --shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); - --shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); - --shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); + --shadow-sm: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow-md: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); + --shadow-lg: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); + --shadow-xl: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); --shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22); } .dark { - --background: oklch(0.26 0.02 352.40); + --background: oklch(0.26 0.02 352.4); --foreground: oklch(0.94 0.01 51.32); --card: oklch(0.32 0.02 341.45); --card-foreground: oklch(0.94 0.01 51.32); --popover: oklch(0.32 0.02 341.45); --popover-foreground: oklch(0.94 0.01 51.32); --primary: oklch(0.57 0.15 35.26); - --primary-foreground: oklch(1.00 0 0); + --primary-foreground: oklch(1 0 0); --secondary: oklch(0.36 0.02 342.27); --secondary-foreground: oklch(0.94 0.01 51.32); --muted: oklch(0.32 0.02 341.45); --muted-foreground: oklch(0.84 0.02 52.63); - --accent: oklch(0.83 0.11 58.00); - --accent-foreground: oklch(0.26 0.02 352.40); + --accent: oklch(0.83 0.11 58); + --accent-foreground: oklch(0.26 0.02 352.4); --destructive: oklch(0.51 0.16 20.19); - --destructive-foreground: oklch(1.00 0 0); + --destructive-foreground: oklch(1 0 0); --border: oklch(0.36 0.02 342.27); --input: oklch(0.36 0.02 342.27); --ring: oklch(0.74 0.16 34.71); --chart-1: oklch(0.74 0.16 34.71); - --chart-2: oklch(0.83 0.11 58.00); + --chart-2: oklch(0.83 0.11 58); --chart-3: oklch(0.88 0.08 54.93); --chart-4: oklch(0.82 0.11 40.89); --chart-5: oklch(0.64 0.13 32.07); - --sidebar: oklch(0.26 0.02 352.40); + --sidebar: oklch(0.26 0.02 352.4); --sidebar-foreground: oklch(0.94 0.01 51.32); --sidebar-primary: oklch(0.47 0.08 34.31); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.67 0.09 56.00); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.67 0.09 56); --sidebar-accent-foreground: oklch(0.26 0.01 353.48); --sidebar-border: oklch(0.36 0.02 342.27); --sidebar-ring: oklch(0.74 0.16 34.71); @@ -92,105 +97,120 @@ --radius: 0.625rem; --shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); --shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); - --shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); - --shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); - --shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); + --shadow-sm: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow-md: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); + --shadow-lg: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); + --shadow-xl: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); --shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22); } .sunset { - --background: oklch(0.98 0.03 80.00); + --background: oklch(0.98 0.03 80); --foreground: oklch(0.34 0.01 2.77); - --card: oklch(1.00 0 0); + --card: oklch(1 0 0); --card-foreground: oklch(0.34 0.01 2.77); - --popover: oklch(1.00 0 0); + --popover: oklch(1 0 0); --popover-foreground: oklch(0.34 0.01 2.77); - --primary: oklch(0.65 0.26 34.00); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.96 0.05 60.00); + --primary: oklch(0.65 0.26 34); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.96 0.05 60); --secondary-foreground: oklch(0.56 0.13 32.74); - --muted: oklch(0.97 0.02 39.40); + --muted: oklch(0.97 0.02 39.4); --muted-foreground: oklch(0.49 0.05 26.45); - --accent: oklch(0.83 0.22 50.00); + --accent: oklch(0.83 0.22 50); --accent-foreground: oklch(0.34 0.01 2.77); --destructive: oklch(0.61 0.21 22.24); - --destructive-foreground: oklch(1.00 0 0); - --border: oklch(0.93 0.06 60.00); - --input: oklch(0.93 0.06 60.00); - --ring: oklch(0.65 0.26 34.00); - --chart-1: oklch(0.65 0.26 34.00); - --chart-2: oklch(0.83 0.22 50.00); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.93 0.06 60); + --input: oklch(0.93 0.06 60); + --ring: oklch(0.65 0.26 34); + --chart-1: oklch(0.65 0.26 34); + --chart-2: oklch(0.83 0.22 50); --chart-3: oklch(0.88 0.15 54.93); - --chart-4: oklch(0.82 0.20 40.89); + --chart-4: oklch(0.82 0.2 40.89); --chart-5: oklch(0.64 0.18 32.07); - --sidebar: oklch(0.97 0.04 70.00); + --sidebar: oklch(0.97 0.04 70); --sidebar-foreground: oklch(0.34 0.01 2.77); - --sidebar-primary: oklch(0.65 0.26 34.00); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.83 0.22 50.00); + --sidebar-primary: oklch(0.65 0.26 34); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.83 0.22 50); --sidebar-accent-foreground: oklch(0.34 0.01 2.77); - --sidebar-border: oklch(0.93 0.06 60.00); - --sidebar-ring: oklch(0.65 0.26 34.00); + --sidebar-border: oklch(0.93 0.06 60); + --sidebar-ring: oklch(0.65 0.26 34); --font-sans: Montserrat, sans-serif; --font-serif: Merriweather, serif; --font-mono: Ubuntu Mono, monospace; --radius: 0.625rem; --shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); --shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); - --shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); - --shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); - --shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); + --shadow-sm: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow-md: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); + --shadow-lg: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); + --shadow-xl: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); --shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22); } .black { - --background: oklch(0.15 0.01 350.00); - --foreground: oklch(0.95 0.01 60.00); - --card: oklch(0.20 0.01 340.00); - --card-foreground: oklch(0.95 0.01 60.00); - --popover: oklch(0.20 0.01 340.00); - --popover-foreground: oklch(0.95 0.01 60.00); - --primary: oklch(0.45 0.10 35.00); - --primary-foreground: oklch(1.00 0 0); - --secondary: oklch(0.25 0.01 340.00); - --secondary-foreground: oklch(0.95 0.01 60.00); - --muted: oklch(0.22 0.01 340.00); - --muted-foreground: oklch(0.86 0.01 60.00); - --accent: oklch(0.70 0.09 58.00); - --accent-foreground: oklch(0.15 0.01 350.00); - --destructive: oklch(0.45 0.16 20.00); - --destructive-foreground: oklch(1.00 0 0); - --border: oklch(0.25 0.01 340.00); - --input: oklch(0.25 0.01 340.00); - --ring: oklch(0.45 0.10 35.00); - --chart-1: oklch(0.45 0.10 35.00); - --chart-2: oklch(0.70 0.09 58.00); - --chart-3: oklch(0.80 0.06 54.00); - --chart-4: oklch(0.75 0.08 40.00); - --chart-5: oklch(0.55 0.10 32.00); - --sidebar: oklch(0.15 0.01 350.00); - --sidebar-foreground: oklch(0.95 0.01 60.00); - --sidebar-primary: oklch(0.40 0.06 34.00); - --sidebar-primary-foreground: oklch(1.00 0 0); - --sidebar-accent: oklch(0.60 0.07 56.00); - --sidebar-accent-foreground: oklch(0.15 0.01 350.00); - --sidebar-border: oklch(0.25 0.01 340.00); - --sidebar-ring: oklch(0.45 0.10 35.00); + --background: oklch(0.15 0.01 350); + --foreground: oklch(0.95 0.01 60); + --card: oklch(0.2 0.01 340); + --card-foreground: oklch(0.95 0.01 60); + --popover: oklch(0.2 0.01 340); + --popover-foreground: oklch(0.95 0.01 60); + --primary: oklch(0.45 0.1 35); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.25 0.01 340); + --secondary-foreground: oklch(0.95 0.01 60); + --muted: oklch(0.22 0.01 340); + --muted-foreground: oklch(0.86 0.01 60); + --accent: oklch(0.7 0.09 58); + --accent-foreground: oklch(0.15 0.01 350); + --destructive: oklch(0.45 0.16 20); + --destructive-foreground: oklch(1 0 0); + --border: oklch(0.25 0.01 340); + --input: oklch(0.25 0.01 340); + --ring: oklch(0.45 0.1 35); + --chart-1: oklch(0.45 0.1 35); + --chart-2: oklch(0.7 0.09 58); + --chart-3: oklch(0.8 0.06 54); + --chart-4: oklch(0.75 0.08 40); + --chart-5: oklch(0.55 0.1 32); + --sidebar: oklch(0.15 0.01 350); + --sidebar-foreground: oklch(0.95 0.01 60); + --sidebar-primary: oklch(0.4 0.06 34); + --sidebar-primary-foreground: oklch(1 0 0); + --sidebar-accent: oklch(0.6 0.07 56); + --sidebar-accent-foreground: oklch(0.15 0.01 350); + --sidebar-border: oklch(0.25 0.01 340); + --sidebar-ring: oklch(0.45 0.1 35); --font-sans: Montserrat, sans-serif; --font-serif: Merriweather, serif; --font-mono: Ubuntu Mono, monospace; --radius: 0.625rem; --shadow-2xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); --shadow-xs: 0px 6px 12px -3px hsl(0 0% 0% / 0.04); - --shadow-sm: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); - --shadow-md: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); - --shadow-lg: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); - --shadow-xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); + --shadow-sm: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 1px 2px -4px hsl(0 0% 0% / 0.09); + --shadow-md: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 2px 4px -4px hsl(0 0% 0% / 0.09); + --shadow-lg: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 4px 6px -4px hsl(0 0% 0% / 0.09); + --shadow-xl: + 0px 6px 12px -3px hsl(0 0% 0% / 0.09), 0px 8px 10px -4px hsl(0 0% 0% / 0.09); --shadow-2xl: 0px 6px 12px -3px hsl(0 0% 0% / 0.22); } @@ -265,8 +285,8 @@ /* Hide scrollbar for IE, Edge and Firefox */ .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ + -ms-overflow-style: none; /* IE and Edge */ /* Use Firefox-specific scrollbar hiding when supported */ scrollbar-width: none; } -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index d980d06..7adabf6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,34 +1,36 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import { ChatSidebar } from "@/components/chat-sidebar"; -import { SidebarTrigger } from "@/components/ui/sidebar"; -import { Menu } from "lucide-react"; -import { Providers } from "./providers"; -import "./globals.css"; -import Script from "next/script"; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import { ChatSidebar } from '@/components/chat-sidebar'; +import { SidebarTrigger } from '@/components/ui/sidebar'; +import { Menu } from 'lucide-react'; +import { Providers } from './providers'; +import './globals.css'; +import Script from 'next/script'; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { - metadataBase: new URL("https://mcp.scira.ai"), - title: "Scira MCP Chat", - description: "Scira MCP Chat is a minimalistic MCP client with a good feature set.", + metadataBase: new URL('https://mcp.scira.ai'), + title: 'Scira MCP Chat', + description: + 'Scira MCP Chat is a minimalistic MCP client with a good feature set.', openGraph: { - siteName: "Scira MCP Chat", - url: "https://mcp.scira.ai", + siteName: 'Scira MCP Chat', + url: 'https://mcp.scira.ai', images: [ { - url: "https://mcp.scira.ai/opengraph-image.png", + url: 'https://mcp.scira.ai/opengraph-image.png', width: 1200, height: 630, }, ], }, twitter: { - card: "summary_large_image", - title: "Scira MCP Chat", - description: "Scira MCP Chat is a minimalistic MCP client with a good feature set.", - images: ["https://mcp.scira.ai/twitter-image.png"], + card: 'summary_large_image', + title: 'Scira MCP Chat', + description: + 'Scira MCP Chat is a minimalistic MCP client with a good feature set.', + images: ['https://mcp.scira.ai/twitter-image.png'], }, }; @@ -51,13 +53,15 @@ export default function RootLayout({ -
- {children} -
+
{children}
-