├── src ├── types │ ├── index.ts │ ├── json.d.ts │ ├── mcp.ts │ ├── llm.config.d.ts │ ├── server.types.ts │ ├── agent.types.ts │ └── systemprompt.d.ts ├── features │ ├── server │ │ ├── __tests__ │ │ │ ├── mocks │ │ │ │ └── nextui.tsx │ │ │ ├── utils.tsx │ │ │ └── PromptCard.test.tsx │ │ ├── api │ │ │ └── config.ts │ │ ├── hooks │ │ │ ├── useModal.ts │ │ │ ├── usePromptLogger.ts │ │ │ └── __tests__ │ │ │ │ ├── useModal.test.ts │ │ │ │ └── usePromptLogger.test.ts │ │ ├── utils │ │ │ └── prompt-utils.ts │ │ └── components │ │ │ └── sections │ │ │ └── ServerHeader.tsx │ ├── agent-registry │ │ └── index.tsx │ ├── llm-registry │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── config.ts │ │ │ └── types.ts │ │ ├── components │ │ │ └── LlmSection.tsx │ │ ├── __tests__ │ │ │ └── LlmRegistryContext.test.tsx │ │ └── contexts │ │ │ └── LlmRegistryContext.tsx │ └── multimodal-agent │ │ ├── Index.tsx │ │ ├── lib │ │ ├── audioworklet-registry.ts │ │ ├── worklets │ │ │ ├── vol-meter.ts │ │ │ └── audio-processing.ts │ │ └── utils.ts │ │ ├── components │ │ └── audio-pulse │ │ │ └── AudioPulse.tsx │ │ ├── __tests__ │ │ └── contexts │ │ │ └── McpContext.test.tsx │ │ └── utils │ │ └── README.md ├── components │ ├── Chip │ │ ├── index.ts │ │ └── EnvVarChip.tsx │ ├── Layout │ │ ├── index.ts │ │ ├── CollapsibleSection.tsx │ │ ├── GridLayout.tsx │ │ ├── PageLayout.tsx │ │ └── Layout.tsx │ ├── Card │ │ ├── index.ts │ │ ├── ServerCard.tsx │ │ ├── PromptCard.tsx │ │ ├── __tests__ │ │ │ ├── ToolCard.test.tsx │ │ │ └── ServerCard.test.tsx │ │ ├── StatusCard.tsx │ │ ├── AccordionCard.tsx │ │ ├── UserInfoCard.tsx │ │ └── BaseCard.tsx │ ├── sidebar │ │ ├── types.ts │ │ ├── index.tsx │ │ └── SidebarItem.tsx │ ├── Button │ │ ├── index.ts │ │ ├── RefreshButton.tsx │ │ ├── SecondaryButton.tsx │ │ ├── StaticButton.tsx │ │ ├── ExecuteButton.tsx │ │ ├── ActionButton.tsx │ │ ├── BaseButton.tsx │ │ └── ConnectButton.tsx │ ├── Link │ │ └── ExternalLink.tsx │ ├── StatusIndicator │ │ ├── ServerConnectionStatus.tsx │ │ ├── StatusIndicator.tsx │ │ └── __tests__ │ │ │ └── StatusIndicator.test.tsx │ ├── shared │ │ ├── Button │ │ │ ├── index.tsx │ │ │ └── BaseButton.tsx │ │ └── Input │ │ │ └── ApiKeyInput.tsx │ └── Modal │ │ ├── hooks │ │ └── useSchemaForm.ts │ │ ├── schema-utils.ts │ │ ├── utils │ │ └── form-state.ts │ │ ├── components │ │ ├── FormField.tsx │ │ └── DiscriminatorField.tsx │ │ └── SamplingModal.tsx ├── vite-env.d.ts ├── contexts │ ├── index.ts │ ├── AuthContext.types.ts │ ├── AppProviders.tsx │ ├── McpContext.tsx │ ├── LlmProviderContext.tsx │ └── api.ts ├── providers │ └── gemini │ │ ├── index.ts │ │ ├── GeminiProvider.tsx │ │ ├── types.ts │ │ ├── provider.ts │ │ └── utils │ │ └── validation.ts ├── pages │ ├── LoggerPage.tsx │ ├── ServerPage.tsx │ └── AgentGalleryPage.tsx ├── env.d.ts ├── tsconfig.node.json ├── main.tsx ├── test │ └── setup.ts ├── utils │ └── env.ts ├── __tests__ │ └── index.test.ts ├── hooks │ ├── useMcpSampling.types.ts │ ├── useMcpSampling.utils.ts │ └── useMcpClient.ts ├── global.css └── stores │ └── log-store.ts ├── proxy ├── dist │ ├── types │ │ └── index.js │ ├── mcpProxy.js │ ├── index.js │ └── server.test.js ├── stdout.txt ├── tsconfig.tsbuildinfo ├── src │ ├── types │ │ ├── spawn-rx.d.ts │ │ ├── api.types.ts │ │ ├── server.types.ts │ │ └── systemprompt.d.ts │ ├── config │ │ └── defaults.ts │ ├── __tests__ │ │ ├── setup.ts │ │ ├── utils │ │ │ └── server.ts │ │ ├── handlers │ │ │ └── configHandlers.test.ts │ │ └── transports │ │ │ └── index.test.ts │ ├── mcpProxy.ts │ └── services │ │ └── McpApiService.ts ├── tsconfig.json ├── tsconfig.test.json ├── jest.config.js ├── vitest.config.ts ├── stderr.txt ├── build │ └── mcpProxy.js └── package.json ├── public ├── icon.png ├── logo.png ├── darklogo.png ├── favicon.ico ├── font │ ├── Zepto.woff │ ├── CustomFont.woff2 │ ├── CustomFont2.woff2 │ └── Zepto-Regular.ttf └── vite.svg ├── TODO.txt ├── postcss.config.js ├── scripts └── google-auth │ ├── credentials │ └── .gitkeep │ ├── tsconfig.json │ ├── package.json │ ├── setup-google-env.js │ └── README.md ├── extensions ├── default.json └── README.md ├── .env.example ├── config ├── mcp.config.example.json ├── README.md ├── agent.config.example.json └── server.config.ts ├── tsconfig.node.json ├── tsconfig.test.json ├── vite.config.ts ├── index.html ├── eslint.config.js ├── LICENSE ├── vitest.config.ts ├── tsconfig.json ├── docs ├── README.md ├── statusindicator-language-models-guide.md ├── layout-language-models-guide.md ├── modal-language-models-guide.md ├── chip-language-models-guide.md ├── agent-registry-language-models-guide.md ├── logger-language-models-guide.md └── button-language-models-guide.md ├── .gitignore └── package.json /src/types/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/server/__tests__/mocks/nextui.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Chip/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FeatureChip"; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /proxy/dist/types/index.js: -------------------------------------------------------------------------------- 1 | export * from "./server.types.js"; 2 | -------------------------------------------------------------------------------- /proxy/stdout.txt: -------------------------------------------------------------------------------- 1 | Terminate batch job (Y/N)? Terminate batch job (Y/N)? -------------------------------------------------------------------------------- /src/components/Layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Layout"; 2 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/icon.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AuthContext"; 2 | export * from "./AuthContext.types"; 3 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | 1. Restart connection when agent is selected 2 | 2. Simplify agent logic, test e2e agent flow -------------------------------------------------------------------------------- /public/darklogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/darklogo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/font/Zepto.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/font/Zepto.woff -------------------------------------------------------------------------------- /src/types/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /proxy/tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/index.ts","./src/mcpproxy.ts","./src/types/spawn-rx.d.ts"],"version":"5.6.3"} -------------------------------------------------------------------------------- /public/font/CustomFont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/font/CustomFont.woff2 -------------------------------------------------------------------------------- /public/font/CustomFont2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/font/CustomFont2.woff2 -------------------------------------------------------------------------------- /public/font/Zepto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/public/font/Zepto-Regular.ttf -------------------------------------------------------------------------------- /scripts/google-auth/credentials/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ejb503/multimodal-mcp-client/HEAD/scripts/google-auth/credentials/.gitkeep -------------------------------------------------------------------------------- /src/types/mcp.ts: -------------------------------------------------------------------------------- 1 | export interface McpMeta { 2 | responseSchema?: Record; 3 | complexResponseSchema?: Record; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BaseCard"; 2 | export * from "./StatusCard"; 3 | export * from "./ServerCard"; 4 | export * from "./ToolCard"; 5 | -------------------------------------------------------------------------------- /src/providers/gemini/index.ts: -------------------------------------------------------------------------------- 1 | export { GeminiProvider } from "./GeminiProvider"; 2 | export { useGeminiProvider } from "./hook"; 3 | export { geminiProvider } from "./provider"; 4 | -------------------------------------------------------------------------------- /extensions/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": [], 3 | "settings": { 4 | "example-extension": { 5 | "enabled": false, 6 | "config": {} 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Client API Keys 2 | VITE_SYSTEMPROMPT_API_KEY=xxx 3 | VITE_GEMINI_API_KEY=xxx 4 | 5 | # Server API Keys 6 | VITE_NOTION_API_KEY=xxx 7 | VITE_GOOGLE_CREDENTIALS=xxx 8 | VITE_GOOGLE_TOKEN=xxx 9 | -------------------------------------------------------------------------------- /src/pages/LoggerPage.tsx: -------------------------------------------------------------------------------- 1 | import { Logger } from "@/components/Logger/Logger"; 2 | 3 | export default function LoggerPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_SYSTEMPROMPT_API_KEY: string; 5 | // Add other env variables here as needed 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /src/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["../vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /proxy/src/types/spawn-rx.d.ts: -------------------------------------------------------------------------------- 1 | declare module "spawn-rx" { 2 | export interface ExecutableResult { 3 | cmd: string; 4 | args: string[]; 5 | } 6 | 7 | export function findActualExecutable( 8 | command: string, 9 | args: string[] 10 | ): ExecutableResult; 11 | } 12 | -------------------------------------------------------------------------------- /src/contexts/AuthContext.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface AuthContextType { 4 | isAuthenticated: boolean; 5 | apiKey: string | null; 6 | setApiKey: (key: string) => void; 7 | } 8 | 9 | export interface AuthProviderProps { 10 | children: ReactNode; 11 | } 12 | -------------------------------------------------------------------------------- /src/providers/gemini/GeminiProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { useGeminiProvider } from "./hook"; 3 | 4 | interface Props { 5 | children: ReactNode; 6 | } 7 | 8 | export function GeminiProvider({ children }: Props) { 9 | useGeminiProvider(); 10 | return <>{children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/llm.config.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*/llm.config.json" { 2 | interface LLMConfig { 3 | provider: string; 4 | config: { 5 | apiKey: string; 6 | model: string; 7 | temperature: number; 8 | maxTokens: number; 9 | }; 10 | } 11 | const config: LLMConfig; 12 | export default config; 13 | } 14 | -------------------------------------------------------------------------------- /proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /config/mcp.config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "systemprompt-mcp-core": { 4 | "id": "xxx", 5 | "env": { 6 | "SYSTEMPROMPT_API_KEY": "xxx", 7 | "xxx": "xxx" 8 | }, 9 | "command": "node", 10 | "args": [ 11 | "/systemprompt-mcp-core/build/index.js" 12 | ] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "outDir": "build/node", 9 | "tsBuildInfoFile": "build/.tsbuildinfo.node" 10 | }, 11 | "include": ["vite.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/features/agent-registry/index.tsx: -------------------------------------------------------------------------------- 1 | // Export components 2 | export { AgentRegistryProvider } from "./contexts/AgentRegistryContext"; 3 | 4 | // Export hooks 5 | export { useAgentRegistry } from "./contexts/AgentRegistryContext"; 6 | 7 | // Export types 8 | export type { 9 | AgentConfig, 10 | AgentRegistryContextType, 11 | } from "../../types/agent.types"; 12 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vitest/globals", "@testing-library/jest-dom"], 5 | "isolatedModules": false 6 | }, 7 | "include": [ 8 | "proxy/**/__tests__/**/*", 9 | "src/**/__tests__/**/*", 10 | "src/**/*.test.*", 11 | "src/**/*.spec.*", 12 | "test/**/*" 13 | ], 14 | "exclude": [] 15 | } 16 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import { NextUIProvider } from "@nextui-org/react"; 5 | import "./global.css"; 6 | 7 | createRoot(document.getElementById("root")!).render( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/features/llm-registry/index.tsx: -------------------------------------------------------------------------------- 1 | // Export components 2 | export { LlmRegistryProvider } from "./contexts/LlmRegistryContext"; 3 | export { LlmSection } from "./components/LlmSection"; 4 | 5 | // Export hooks 6 | export { useLlmRegistry } from "./contexts/LlmRegistryContext"; 7 | 8 | // Export types 9 | export type { 10 | LlmProviderConfig, 11 | LlmProviderInstance, 12 | LlmRegistryContextType, 13 | } from "./lib/types"; 14 | -------------------------------------------------------------------------------- /proxy/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vitest/globals"], 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "strictPropertyInitialization": false, 8 | "strictNullChecks": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false 11 | }, 12 | "include": ["src/**/*.test.ts", "src/**/*.spec.ts"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { expect, afterEach } from "vitest"; 3 | import { cleanup } from "@testing-library/react"; 4 | import * as matchers from "@testing-library/jest-dom/matchers"; 5 | 6 | // Extend Vitest's expect method with methods from react-testing-library 7 | expect.extend(matchers); 8 | 9 | // Cleanup after each test case (e.g. clearing jsdom) 10 | afterEach(() => { 11 | cleanup(); 12 | }); 13 | -------------------------------------------------------------------------------- /scripts/google-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "outDir": "dist" 13 | }, 14 | "include": ["*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/sidebar/types.ts: -------------------------------------------------------------------------------- 1 | import { ServerMetadata } from "../../../config/types"; 2 | 3 | export interface SidebarItem { 4 | key: string; 5 | label: string; 6 | icon?: string; 7 | color?: "success" | "warning" | "primary" | "secondary"; 8 | href?: string; 9 | description?: string; 10 | serverId?: string; 11 | metadata?: ServerMetadata; 12 | } 13 | 14 | export interface SidebarSection { 15 | title: string; 16 | items: SidebarItem[]; 17 | } 18 | -------------------------------------------------------------------------------- /scripts/google-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-auth-setup", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "ts-node --esm auth-google.ts" 8 | }, 9 | "dependencies": { 10 | "googleapis": "^129.0.0", 11 | "google-auth-library": "^9.6.3" 12 | }, 13 | "devDependencies": { 14 | "ts-node": "^10.9.2", 15 | "typescript": "^5.3.3", 16 | "@types/node": "^20.11.19" 17 | } 18 | } -------------------------------------------------------------------------------- /src/contexts/AppProviders.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { NextUIProvider } from "@nextui-org/react"; 3 | import { McpProvider } from "@/contexts/McpContext"; 4 | import { LiveAPIProvider } from "@/features/multimodal-agent/contexts/LiveAPIContext"; 5 | 6 | interface AppProvidersProps { 7 | children: ReactNode; 8 | } 9 | 10 | export function AppProviders({ children }: AppProvidersProps) { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /extensions/README.md: -------------------------------------------------------------------------------- 1 | # Extensions Directory 2 | 3 | This directory contains extension configurations for the application. To set up your local extensions: 4 | 5 | 1. Copy the default configuration file to create your local version: 6 | 7 | ```bash 8 | cp default.json extensions.json 9 | ``` 10 | 11 | 2. Modify the copied file to enable/configure your desired extensions. 12 | 13 | ## Structure 14 | 15 | - `default.json`: Template showing the expected structure 16 | The `default.json` file is tracked in git as a template. Your local `extensions.json` will be ignored by git to keep your settings private. 17 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | "@": path.resolve(__dirname, "./src"), 10 | "@config": path.resolve(__dirname, "./config"), 11 | }, 12 | }, 13 | optimizeDeps: { 14 | force: true, 15 | }, 16 | server: { 17 | proxy: { 18 | "/v1": { 19 | target: "http://localhost:3000", 20 | changeOrigin: true, 21 | }, 22 | }, 23 | fs: { 24 | strict: true, 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/features/server/__tests__/utils.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import { NextUIProvider } from "@nextui-org/react"; 3 | import { McpProvider } from "@/contexts/McpContext"; 4 | 5 | export const renderWithProviders = (ui: React.ReactElement) => { 6 | return render( 7 | 8 | {ui} 9 | 10 | ); 11 | }; 12 | 13 | export const mockServerInfo = { 14 | name: "Test Server", 15 | version: "1.0.0", 16 | protocolVersion: "1.0", 17 | capabilities: { 18 | tools: true, 19 | prompts: true, 20 | resources: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /proxy/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | roots: ["/src"], 5 | testMatch: ["**/__tests__/**/*.test.ts"], 6 | ignorePatterns: ["node_modules", "dist", "build"], 7 | moduleFileExtensions: ["ts", "js", "json", "node"], 8 | collectCoverage: true, 9 | coverageDirectory: "coverage", 10 | coverageReporters: ["text", "lcov"], 11 | coveragePathIgnorePatterns: ["/node_modules/", "/build/", "/dist/"], 12 | coverageThreshold: { 13 | global: { 14 | branches: 80, 15 | functions: 80, 16 | lines: 80, 17 | statements: 80, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Systemprompt MCP Client 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /proxy/src/config/defaults.ts: -------------------------------------------------------------------------------- 1 | import { ServerDefaults } from "../types/index.js"; 2 | 3 | export const defaults: ServerDefaults = { 4 | serverTypes: { 5 | stdio: { 6 | icon: "solar:server-square-line-duotone", 7 | color: "primary", 8 | description: "Local MCP Server", 9 | serverType: "core", 10 | }, 11 | sse: { 12 | icon: "solar:server-square-cloud-bold-duotone", 13 | color: "primary", 14 | description: "Remote SSE-based MCP Server", 15 | serverType: "core", 16 | }, 17 | }, 18 | unconnected: { 19 | icon: "solar:server-square-line-duotone", 20 | color: "warning", 21 | description: "Disconnected Server", 22 | serverType: "core", 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /proxy/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | include: ["src/**/*.test.ts"], 8 | testTimeout: 1000, 9 | setupFiles: ["src/__tests__/setup.ts"], 10 | coverage: { 11 | provider: "v8", 12 | reporter: ["text", "json", "html"], 13 | exclude: [ 14 | "node_modules/**", 15 | "src/**/*.test.ts", 16 | "src/types/**", 17 | "build/**", 18 | "dist/**", 19 | ], 20 | thresholds: { 21 | branches: 80, 22 | functions: 80, 23 | lines: 80, 24 | statements: 80, 25 | }, 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | import { Button } from "@nextui-org/react"; 2 | 3 | // Base components 4 | export { BaseButton } from "./Button"; 5 | export type { BaseButtonProps } from "./Button"; 6 | export { Button }; 7 | 8 | // Variant components 9 | export { StaticButton } from "./Button"; 10 | export type { StaticButtonProps } from "./Button"; 11 | export { DynamicButton } from "./DynamicButton"; 12 | export type { DynamicButtonProps } from "./DynamicButton"; 13 | export { ExecuteButton } from "./ExecuteButton"; 14 | export { RefreshButton } from "./RefreshButton"; 15 | 16 | // Legacy components (to be migrated) 17 | export { ConnectButton } from "./ConnectButton"; 18 | export { SecondaryButton } from "./SecondaryButton"; 19 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/Index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import ControlTray from "./components/control-tray/ControlTray"; 3 | import ConfigCard from "./components/config-card/ConfigCard"; 4 | 5 | function MultiModalAgent() { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | ); 21 | } 22 | 23 | export default MultiModalAgent; 24 | -------------------------------------------------------------------------------- /src/components/Button/RefreshButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import { DynamicButton } from "./DynamicButton"; 3 | import type { ButtonProps } from "@nextui-org/react"; 4 | 5 | interface RefreshButtonProps extends Omit { 6 | loading?: boolean; 7 | } 8 | 9 | export const RefreshButton = forwardRef( 10 | ({ loading = false, ...props }, ref) => { 11 | return ( 12 | 20 | ); 21 | } 22 | ); 23 | 24 | RefreshButton.displayName = "RefreshButton"; 25 | -------------------------------------------------------------------------------- /src/features/llm-registry/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { LlmConfig } from "./types"; 2 | 3 | const CONFIG_KEY = "llm-settings"; 4 | 5 | export async function loadLlmSettings(): Promise { 6 | try { 7 | const stored = localStorage.getItem(CONFIG_KEY); 8 | if (stored) { 9 | return JSON.parse(stored); 10 | } 11 | } catch (error) { 12 | console.error("Failed to load LLM settings:", error); 13 | } 14 | 15 | // Default settings 16 | return { 17 | provider: "gemini", 18 | config: {}, 19 | }; 20 | } 21 | 22 | export async function saveLlmSettings(settings: LlmConfig): Promise { 23 | try { 24 | localStorage.setItem(CONFIG_KEY, JSON.stringify(settings)); 25 | } catch (error) { 26 | console.error("Failed to save LLM settings:", error); 27 | throw error; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/lib/audioworklet-registry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A registry to map attached worklets by their audio-context 3 | * any module using `audioContext.audioWorklet.addModule(` should register the worklet here 4 | */ 5 | export type WorkletGraph = { 6 | node?: AudioWorkletNode; 7 | handlers: Array<(this: MessagePort, ev: MessageEvent) => any>; 8 | }; 9 | 10 | export const registeredWorklets: Map< 11 | AudioContext, 12 | Record 13 | > = new Map(); 14 | 15 | export const createWorketFromSrc = ( 16 | workletName: string, 17 | workletSrc: string 18 | ) => { 19 | const script = new Blob( 20 | [`registerProcessor("${workletName}", ${workletSrc})`], 21 | { 22 | type: "application/javascript", 23 | } 24 | ); 25 | 26 | return URL.createObjectURL(script); 27 | }; 28 | -------------------------------------------------------------------------------- /src/providers/gemini/types.ts: -------------------------------------------------------------------------------- 1 | export interface GeminiCandidate { 2 | content: { 3 | parts: Array<{ 4 | text: string; 5 | }>; 6 | role: string; 7 | }; 8 | finishReason: string; 9 | index: number; 10 | safetyRatings: Array<{ 11 | category: string; 12 | probability: string; 13 | }>; 14 | } 15 | 16 | export interface GeminiResponse { 17 | candidates?: GeminiCandidate[]; 18 | error?: string; 19 | } 20 | 21 | export interface LlmResponse { 22 | response: string; 23 | error?: string; 24 | } 25 | 26 | export interface GeminiConfig { 27 | model?: string; 28 | temperature?: number; 29 | maxTokens?: number; 30 | apiKey?: string; 31 | _meta?: { 32 | responseSchema?: Record; 33 | complexResponseSchema?: Record; 34 | callback?: string; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /proxy/stderr.txt: -------------------------------------------------------------------------------- 1 | 2025-01-21T12:25:31.752Z agentkeepalive sock[0#registry.npmjs.org:443::::::::true:::::::::::::] create, timeout 300001ms 2 | 2025-01-21T12:25:32.314Z agentkeepalive sock[0#registry.npmjs.org:443::::::::true:::::::::::::](requests: 1, finished: 1) free 3 | 2025-01-21T12:25:47.326Z agentkeepalive sock[0#registry.npmjs.org:443::::::::true:::::::::::::](requests: 1, finished: 1) timeout after 15000ms, listeners 2, defaultTimeoutListenerCount 3, hasHttpRequest false, HttpRequest timeoutListenerCount 0 4 | 2025-01-21T12:25:47.336Z agentkeepalive timeout listeners: onTimeout, onTimeout 5 | 2025-01-21T12:25:47.338Z agentkeepalive sock[0#registry.npmjs.org:443::::::::true:::::::::::::] is free, destroy quietly 6 | 2025-01-21T12:25:47.340Z agentkeepalive sock[0#registry.npmjs.org:443::::::::true:::::::::::::](requests: 1, finished: 1) close, isError: false 7 | ^C^C -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /src/features/llm-registry/components/LlmSection.tsx: -------------------------------------------------------------------------------- 1 | import { useLlmRegistry } from "../contexts/LlmRegistryContext"; 2 | import { LlmConfigCard } from "./LlmConfigCard"; 3 | 4 | export function LlmSection() { 5 | const { activeProvider, providerConfig, getProviderConfig, providers } = 6 | useLlmRegistry(); 7 | 8 | const currentProvider = activeProvider 9 | ? getProviderConfig(activeProvider) 10 | : null; 11 | 12 | // Determine the display state 13 | const isProviderConfigured = activeProvider !== null; 14 | const isProviderRegistered = providers.some((p) => p.id === activeProvider); 15 | const isProviderMissing = isProviderConfigured && !isProviderRegistered; 16 | 17 | return ( 18 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /proxy/src/types/api.types.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig, ServerDefaults } from "./server.types.js"; 2 | 3 | export interface ApiServerMetadata { 4 | icon?: string; 5 | color?: string; 6 | description?: string; 7 | serverType?: string; 8 | } 9 | 10 | export interface ApiServerConfig { 11 | id: string; 12 | status: "connected" | "disconnected" | "error"; 13 | error?: string; 14 | command: string; 15 | args: string[]; 16 | env?: string[]; 17 | metadata: { 18 | icon: string; 19 | color: string; 20 | description: string; 21 | serverType: "core" | "custom"; 22 | }; 23 | } 24 | 25 | export interface ApiResponse { 26 | success: boolean; 27 | error?: string; 28 | body: T; 29 | } 30 | 31 | export interface TransformedMcpData { 32 | mcpServers: Record; 33 | available: Record; 34 | defaults: ServerDefaults; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Button/SecondaryButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | 4 | interface SecondaryButtonProps extends Omit { 5 | icon?: string; 6 | iconClassName?: string; 7 | } 8 | 9 | export function SecondaryButton({ 10 | children, 11 | icon, 12 | iconClassName = "", 13 | className = "", 14 | ...props 15 | }: SecondaryButtonProps) { 16 | return ( 17 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/ServerPage.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { useMcp } from "@/contexts/McpContext"; 3 | import { ServerPageContent } from "@/features/server/components/ServerPageContent"; 4 | import { StatusIndicator } from "@/components/StatusIndicator/StatusIndicator"; 5 | 6 | export function ServerPage() { 7 | const { serverId } = useParams<{ serverId: string }>(); 8 | const { clients } = useMcp(); 9 | 10 | if (!serverId) { 11 | return ( 12 |
13 | 18 |
19 | ); 20 | } 21 | 22 | const clientState = clients[serverId]; 23 | const serverName = clientState?.serverInfo?.name || serverId; 24 | 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const envSchema = z.object({ 4 | VITE_SYSTEMPROMPT_API_KEY: z.string(), 5 | VITE_GEMINI_API_KEY: z.string(), 6 | }); 7 | 8 | export type EnvConfig = z.infer; 9 | 10 | export function loadEnvConfig(): EnvConfig { 11 | const env: Record = {}; 12 | 13 | // Get all environment variables that start with VITE_ 14 | Object.keys(import.meta.env).forEach((key) => { 15 | if (key.startsWith("VITE_")) { 16 | env[key] = import.meta.env[key]; 17 | } 18 | }); 19 | 20 | // Validate against schema 21 | const result = envSchema.safeParse(env); 22 | 23 | if (!result.success) { 24 | console.error("Environment validation failed:", result.error.format()); 25 | throw new Error("Missing required environment variables"); 26 | } 27 | 28 | return result.data; 29 | } 30 | 31 | // Export a singleton instance 32 | export const envConfig = loadEnvConfig(); 33 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, vi } from "vitest"; 2 | import { parseArgs } from "node:util"; 3 | 4 | vi.mock("node:util", () => ({ 5 | parseArgs: vi.fn((config) => ({ 6 | values: { port: config?.options?.port?.default || "3000" }, 7 | positionals: [], 8 | })), 9 | })); 10 | 11 | describe("main function", () => { 12 | beforeEach(() => { 13 | vi.resetModules(); 14 | process.argv = ["node", "index.js"]; 15 | vi.clearAllMocks(); 16 | }); 17 | 18 | it("should start the server with default port", async () => { 19 | // ... existing code ... 20 | }); 21 | 22 | it("should start the server with custom port", async () => { 23 | process.argv = ["node", "index.js", "--port", "4000"]; 24 | vi.mocked(parseArgs).mockImplementationOnce((config) => ({ 25 | values: { port: "4000" }, 26 | positionals: [], 27 | })); 28 | // ... existing code ... 29 | }); 30 | // ... existing code ... 31 | }); 32 | -------------------------------------------------------------------------------- /src/types/server.types.ts: -------------------------------------------------------------------------------- 1 | import { SystempromptAgent, SystempromptModule } from "./systemprompt"; 2 | 3 | export interface ServerMetadata { 4 | icon?: string; 5 | color?: string; 6 | description?: string; 7 | serverType?: "core" | "custom"; 8 | name?: string; 9 | } 10 | 11 | export interface ServerConfig { 12 | command: string; 13 | args: string[]; 14 | env: Record; 15 | metadata?: ServerMetadata; 16 | } 17 | 18 | export interface ServerDefaults { 19 | serverTypes: { 20 | stdio: ServerMetadata; 21 | sse: ServerMetadata; 22 | }; 23 | unconnected: ServerMetadata; 24 | } 25 | 26 | export interface McpModuleMetadata { 27 | created: string; 28 | updated: string; 29 | version: number; 30 | status: string; 31 | } 32 | 33 | export interface McpData { 34 | mcpServers: Record; 35 | defaults: ServerDefaults; 36 | available: Record; 37 | agents: SystempromptAgent[]; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Button/StaticButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import { motion } from "framer-motion"; 3 | import { BaseButton, BaseButtonProps } from "./BaseButton"; 4 | 5 | export interface StaticButtonProps extends BaseButtonProps { 6 | instant?: boolean; 7 | } 8 | 9 | export const StaticButton = forwardRef( 10 | ({ instant = true, className = "", label, ...props }, ref) => { 11 | return ( 12 | 16 | 26 | 27 | ); 28 | } 29 | ); 30 | 31 | StaticButton.displayName = "StaticButton"; 32 | -------------------------------------------------------------------------------- /proxy/build/mcpProxy.js: -------------------------------------------------------------------------------- 1 | export default function mcpProxy({ transportToClient, transportToServer, onerror, }) { 2 | let transportToClientClosed = false; 3 | let transportToServerClosed = false; 4 | transportToClient.onmessage = (message) => { 5 | transportToServer.send(message).catch(onerror); 6 | }; 7 | transportToServer.onmessage = (message) => { 8 | transportToClient.send(message).catch(onerror); 9 | }; 10 | transportToClient.onclose = () => { 11 | if (transportToServerClosed) { 12 | return; 13 | } 14 | transportToClientClosed = true; 15 | transportToServer.close().catch(onerror); 16 | }; 17 | transportToServer.onclose = () => { 18 | if (transportToClientClosed) { 19 | return; 20 | } 21 | transportToServerClosed = true; 22 | transportToClient.close().catch(onerror); 23 | }; 24 | transportToClient.onerror = onerror; 25 | transportToServer.onerror = onerror; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Button/ExecuteButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import { DynamicButton } from "./DynamicButton"; 3 | import type { ButtonProps } from "@nextui-org/react"; 4 | 5 | interface ExecuteButtonProps extends Omit { 6 | loading?: boolean; 7 | label?: string; 8 | loadingLabel?: string; 9 | } 10 | 11 | export const ExecuteButton = forwardRef( 12 | ( 13 | { 14 | loading = false, 15 | label = "Execute", 16 | loadingLabel = "Executing...", 17 | ...props 18 | }, 19 | ref 20 | ) => { 21 | return ( 22 | 31 | {label} 32 | 33 | ); 34 | } 35 | ); 36 | 37 | ExecuteButton.displayName = "ExecuteButton"; 38 | -------------------------------------------------------------------------------- /src/components/Button/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, ButtonProps } from "@nextui-org/react"; 3 | import { Icon } from "@iconify/react"; 4 | 5 | interface ActionButtonProps extends Omit { 6 | icon?: string; 7 | label?: string; 8 | isIconOnly?: boolean; 9 | } 10 | 11 | /** 12 | * ActionButton component that provides consistent styling for action buttons 13 | */ 14 | export function ActionButton({ 15 | icon, 16 | label, 17 | isIconOnly = false, 18 | className = "", 19 | ...props 20 | }: ActionButtonProps) { 21 | if (isIconOnly) { 22 | return ( 23 | 30 | ); 31 | } 32 | 33 | return ( 34 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/AgentGalleryPage.tsx: -------------------------------------------------------------------------------- 1 | import { useAgentRegistry, AgentConfig } from "@/features/agent-registry"; 2 | import { AgentCard } from "@/components/Card/AgentCard"; 3 | 4 | export default function AgentGalleryPage() { 5 | const { agents, activeAgent, setActiveAgent } = useAgentRegistry(); 6 | const handleSetActive = (agent: AgentConfig) => { 7 | setActiveAgent(agent.id); 8 | }; 9 | 10 | return ( 11 |
12 |
13 |
14 |

Agent Gallery

15 |

16 | Manage and interact with your AI agents 17 |

18 |
19 |
20 |
21 | {agents.map((agent) => ( 22 | 28 | ))} 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@systemprompt-voice-mcp/proxy", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "tsx src/index.ts", 7 | "build": "tsc", 8 | "test": "vitest", 9 | "test:coverage": "vitest run --coverage", 10 | "lint": "eslint src/**/*.ts", 11 | "format": "prettier --write src/**/*.ts" 12 | }, 13 | "dependencies": { 14 | "@inquirer/prompts": "^3.3.0", 15 | "@modelcontextprotocol/sdk": "^1.0.4", 16 | "axios": "^1.7.9", 17 | "chalk": "^5.3.0", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.4.7", 20 | "eventsource": "^3.0.2", 21 | "express": "^4.21.2", 22 | "ora": "^8.0.1", 23 | "spawn-rx": "^5.1.1" 24 | }, 25 | "devDependencies": { 26 | "@types/cors": "^2.8.17", 27 | "@types/eventsource": "^1.1.15", 28 | "@types/express": "^4.17.21", 29 | "@types/node": "^20.11.30", 30 | "@types/supertest": "^6.0.2", 31 | "@vitest/coverage-v8": "^1.4.0", 32 | "prettier": "^3.2.5", 33 | "supertest": "^6.3.4", 34 | "tsx": "^4.7.1", 35 | "typescript": "^5.4.2", 36 | "vitest": "^1.4.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Systemprompt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /proxy/src/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | import type { ProxyServer } from "../server.js"; 3 | 4 | // Mock console methods to keep test output clean 5 | vi.spyOn(console, "log").mockImplementation(() => {}); 6 | vi.spyOn(console, "error").mockImplementation(() => {}); 7 | vi.spyOn(console, "warn").mockImplementation(() => {}); 8 | 9 | // Configure vitest for our tests 10 | vi.mock("@modelcontextprotocol/sdk/client/stdio.js"); 11 | vi.mock("@modelcontextprotocol/sdk/server/sse.js"); 12 | vi.mock("spawn-rx"); 13 | vi.mock("axios"); 14 | 15 | // Allow accessing private methods in tests 16 | vi.mock("../server.js", async (importOriginal) => { 17 | const mod = (await importOriginal()) as { ProxyServer: typeof ProxyServer }; 18 | const originalProxyServer = mod.ProxyServer; 19 | return { 20 | ...mod, 21 | ProxyServer: function (...args: ConstructorParameters) { 22 | const instance = new originalProxyServer(...args); 23 | return Object.assign(instance, { 24 | handleSSE: instance["handleSSE"].bind(instance), 25 | }); 26 | } as unknown as typeof ProxyServer, 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /src/contexts/McpContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This file defines the MCP (Model Context Protocol) React context. 3 | * The actual implementation is in McpProvider.tsx. 4 | * 5 | * @see McpProvider.tsx for the implementation 6 | */ 7 | import { createContext, useContext } from "react"; 8 | import type { McpContextType } from "../types/McpContext.types"; 9 | 10 | // Re-export the types 11 | export type { McpContextType }; 12 | 13 | // Create the context with a helpful error message if used outside provider 14 | export const McpContext = createContext(null); 15 | 16 | /** 17 | * Hook to access the MCP context. 18 | * Must be used within an McpProvider. 19 | * 20 | * @throws {Error} If used outside of an McpProvider 21 | */ 22 | export function useMcp() { 23 | const context = useContext(McpContext); 24 | if (!context) { 25 | throw new Error( 26 | "useMcp must be used within McpProvider. " + 27 | "Make sure you have wrapped your app with ." 28 | ); 29 | } 30 | return context; 31 | } 32 | 33 | // Re-export the provider from its implementation file 34 | export { McpProvider } from "./McpProvider"; 35 | -------------------------------------------------------------------------------- /proxy/src/__tests__/utils/server.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, afterAll, afterEach, vi } from "vitest"; 2 | import { Express } from "express"; 3 | import supertest from "supertest"; 4 | import type { McpConfig } from "../../types/index.js"; 5 | import { ProxyServer } from "../../server.js"; 6 | 7 | export const createTestServer = ( 8 | config: Partial = {} 9 | ): { 10 | app: Express; 11 | server: ProxyServer; 12 | } => { 13 | const server = new ProxyServer(config as McpConfig); 14 | return { 15 | app: server.getExpressApp(), 16 | server, 17 | }; 18 | }; 19 | 20 | export const createTestClient = (app: Express) => { 21 | return supertest(app); 22 | }; 23 | 24 | // Mock console methods to keep test output clean 25 | beforeAll(() => { 26 | vi.spyOn(console, "log").mockImplementation(() => {}); 27 | vi.spyOn(console, "error").mockImplementation(() => {}); 28 | vi.spyOn(console, "warn").mockImplementation(() => {}); 29 | }); 30 | 31 | // Clear all mocks after each test 32 | afterEach(() => { 33 | vi.clearAllMocks(); 34 | }); 35 | 36 | // Restore console methods after all tests 37 | afterAll(() => { 38 | vi.restoreAllMocks(); 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/Card/ServerCard.tsx: -------------------------------------------------------------------------------- 1 | import { BaseCard, BaseCardProps } from "./BaseCard"; 2 | 3 | export interface ServerCardProps extends Omit { 4 | serverName: string; 5 | serverId: string; 6 | isConnected: boolean; 7 | } 8 | 9 | export function ServerCard({ 10 | serverName, 11 | serverId, 12 | isConnected, 13 | className = "", 14 | ...props 15 | }: ServerCardProps) { 16 | return ( 17 | 23 |

{serverName}

24 | 25 | } 26 | subtitle={ 27 |

28 | Server ID:{" "} 29 | 30 | {serverId} 31 | 32 |

33 | } 34 | className={`w-full ${className}`} 35 | {...props} 36 | /> 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/providers/gemini/provider.ts: -------------------------------------------------------------------------------- 1 | import { LlmProviderConfig } from "@/features/llm-registry/lib/types"; 2 | 3 | export const geminiProvider: LlmProviderConfig = { 4 | id: "gemini", 5 | name: "Google Gemini Flash", 6 | description: "Google's Gemini Flash language model", 7 | configSchema: { 8 | apiKey: { 9 | type: "string", 10 | label: "API Key", 11 | description: 12 | "Your Google Gemini API key (optional if set in VITE_GEMINI_API_KEY environment variable)", 13 | isSecret: true, 14 | }, 15 | model: { 16 | type: "select", 17 | label: "Model", 18 | description: "The Gemini model to use", 19 | default: "gemini-2.0-flash-exp", 20 | options: [{ label: "Gemini Flash", value: "gemini-2.0-flash-exp" }], 21 | }, 22 | temperature: { 23 | type: "number", 24 | label: "Temperature", 25 | description: "Controls randomness in the model's output", 26 | default: 0.7, 27 | }, 28 | maxTokens: { 29 | type: "number", 30 | label: "Max Tokens", 31 | description: "Maximum number of tokens to generate", 32 | default: 1000, 33 | }, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vitest/config"; 3 | import { configDefaults } from "vitest/config"; 4 | import react from "@vitejs/plugin-react"; 5 | import path from "path"; 6 | 7 | export default defineConfig({ 8 | plugins: [react()], 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "./src"), 12 | "@config": path.resolve(__dirname, "./config"), 13 | }, 14 | }, 15 | test: { 16 | globals: true, 17 | environment: "jsdom", 18 | setupFiles: ["./src/test/setup.ts"], 19 | include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], 20 | coverage: { 21 | provider: "v8", 22 | reporter: ["text", "json", "html"], 23 | exclude: [ 24 | ...(configDefaults.coverage.exclude || []), 25 | "coverage/**", 26 | "dist/**", 27 | "**/[.]**", 28 | "packages/*/test?(s)/**", 29 | "**/*.d.ts", 30 | "**/virtual:*", 31 | "**/__mocks__/*", 32 | ], 33 | }, 34 | typecheck: { 35 | tsconfig: "./tsconfig.test.json", 36 | include: ["src/**/*.{test,spec}.{ts,tsx}"], 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # MCP Configuration 2 | 3 | ## Initial Setup 4 | 5 | Before running the application, you need to set up your local configuration files: 6 | 7 | 1. In the `config` directory, create your local configuration files by removing the `.example` suffix from the template files and replacing with `.custom`: 8 | 9 | ```bash 10 | cp mcp.config.example.json mcp.config.custom.json 11 | cp agent.config.example.json agent.config.custom.json 12 | ``` 13 | 14 | 2. Edit each configuration file according to your needs: 15 | - `mcp.config.json`: Configure your MCP server connections 16 | - `agent.config.json`: Set up your agent configurations 17 | 18 | > Note: The `.example` files are templates tracked in git. Your local copies (without `.example`) are git-ignored to keep your settings private. 19 | 20 | This directory contains configuration files for Model Context Protocol (MCP) servers and their connections. The configuration supports two types of MCP servers: 21 | 22 | 1. **SSE Servers** (`sse`): Remote servers that communicate directly via Server-Sent Events (SSE). [Coming Soon] 23 | 2. **stdio Servers** (`mcpServers`): Local servers that run as child processes and communicate through standard I/O. -------------------------------------------------------------------------------- /src/components/Chip/EnvVarChip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chip, Tooltip } from "@nextui-org/react"; 3 | import { Icon } from "@iconify/react"; 4 | 5 | interface EnvVarChipProps { 6 | name: string; 7 | className?: string; 8 | tooltipContent?: string; 9 | variant?: 10 | | "flat" 11 | | "solid" 12 | | "dot" 13 | | "bordered" 14 | | "light" 15 | | "shadow" 16 | | "faded"; 17 | size?: "sm" | "md" | "lg"; 18 | } 19 | 20 | /** 21 | * EnvVarChip displays environment variables with consistent styling and tooltips 22 | */ 23 | export function EnvVarChip({ 24 | name, 25 | className = "", 26 | tooltipContent = "Required environment variable", 27 | variant = "flat", 28 | size = "sm", 29 | }: EnvVarChipProps) { 30 | return ( 31 | 32 | 41 | } 42 | > 43 | {name} 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitThis": true, 20 | "noImplicitReturns": true, 21 | "noUncheckedIndexedAccess": true, 22 | "allowJs": true, 23 | "checkJs": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "baseUrl": ".", 26 | "outDir": "build/dist", 27 | "incremental": true, 28 | "tsBuildInfoFile": "build/.tsbuildinfo", 29 | "paths": { 30 | "@/*": ["src/*"], 31 | "@config/*": ["config/*"] 32 | } 33 | }, 34 | "include": ["src/**/*", "vite.config.ts"], 35 | "exclude": ["src/**/__tests__/**/*", "src/**/*.test.*", "src/**/*.spec.*", "node_modules"], 36 | "references": [{ "path": "./tsconfig.node.json" }] 37 | } 38 | -------------------------------------------------------------------------------- /src/features/server/api/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { LlmConfig } from "@/features/llm-registry/lib/types"; 4 | 5 | const CONFIG_DIR = path.join(process.cwd(), "config"); 6 | const LLM_CONFIG_PATH = path.join(CONFIG_DIR, "llm.config.json"); 7 | 8 | export async function getLlmConfig(): Promise { 9 | try { 10 | const configData = await fs.promises.readFile(LLM_CONFIG_PATH, "utf-8"); 11 | return JSON.parse(configData); 12 | } catch (error) { 13 | console.error("Error reading LLM config:", error); 14 | // Return default config if file doesn't exist 15 | return { 16 | provider: "gemini", 17 | config: { 18 | apiKey: "", 19 | model: "gemini-pro", 20 | temperature: 0.7, 21 | maxTokens: 1000, 22 | }, 23 | }; 24 | } 25 | } 26 | 27 | export async function updateLlmConfig(config: LlmConfig): Promise { 28 | try { 29 | if (!fs.existsSync(CONFIG_DIR)) { 30 | await fs.promises.mkdir(CONFIG_DIR, { recursive: true }); 31 | } 32 | await fs.promises.writeFile( 33 | LLM_CONFIG_PATH, 34 | JSON.stringify(config, null, 2), 35 | "utf-8" 36 | ); 37 | } catch (error) { 38 | console.error("Error writing LLM config:", error); 39 | throw error; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation Directory 2 | 3 | This directory contains organized copies of all documentation files from across the project. The files are automatically generated using the `scripts/organize-docs.js` script. 4 | 5 | ## File Organization 6 | 7 | - Each file in this directory is a copy of a documentation file from elsewhere in the project 8 | - Files are renamed using a semantic naming scheme based on their location and purpose 9 | - Each file contains a reference comment at the top indicating its original location 10 | - README files are converted to `{directory}-guide.md` format 11 | - Component and feature documentation includes their context in the filename 12 | 13 | ## Naming Convention 14 | 15 | - Files use kebab-case naming (e.g., `feature-name-guide.md`) 16 | - README.md files are renamed to `{context}-guide.md` (e.g., `config-guide.md`) 17 | - Path context is included when needed to avoid duplicates 18 | - Special characters are converted to readable text 19 | 20 | ## Regenerating Documentation 21 | 22 | To regenerate this documentation: 23 | 24 | ```bash 25 | node scripts/organize-docs.js 26 | ``` 27 | 28 | This will: 29 | 30 | 1. Find all markdown files in the project 31 | 2. Create semantic copies in this directory 32 | 3. Add source reference comments 33 | 4. Handle naming conflicts automatically 34 | -------------------------------------------------------------------------------- /src/components/Card/PromptCard.tsx: -------------------------------------------------------------------------------- 1 | import type { Prompt as McpPrompt } from "@modelcontextprotocol/sdk/types.js"; 2 | import { ToolCard } from "@/components/Card"; 3 | import { ExecuteButton, Button } from "@/components/Button"; 4 | 5 | interface PromptCardProps { 6 | prompt: McpPrompt; 7 | onExecute: () => void; 8 | onView: () => void; 9 | isLoading?: boolean; 10 | } 11 | 12 | export function PromptCard({ 13 | prompt, 14 | onExecute, 15 | onView, 16 | isLoading = false, 17 | }: PromptCardProps) { 18 | return ( 19 |
20 | 25 |
26 | 34 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/lib/worklets/vol-meter.ts: -------------------------------------------------------------------------------- 1 | const VolMeterWorket = ` 2 | class VolMeter extends AudioWorkletProcessor { 3 | volume 4 | updateIntervalInMS 5 | nextUpdateFrame 6 | 7 | constructor() { 8 | super() 9 | this.volume = 0 10 | this.updateIntervalInMS = 25 11 | this.nextUpdateFrame = this.updateIntervalInMS 12 | this.port.onmessage = event => { 13 | if (event.data.updateIntervalInMS) { 14 | this.updateIntervalInMS = event.data.updateIntervalInMS 15 | } 16 | } 17 | } 18 | 19 | get intervalInFrames() { 20 | return (this.updateIntervalInMS / 1000) * sampleRate 21 | } 22 | 23 | process(inputs) { 24 | const input = inputs[0] 25 | 26 | if (input.length > 0) { 27 | const samples = input[0] 28 | let sum = 0 29 | let rms = 0 30 | 31 | for (let i = 0; i < samples.length; ++i) { 32 | sum += samples[i] * samples[i] 33 | } 34 | 35 | rms = Math.sqrt(sum / samples.length) 36 | this.volume = Math.max(rms, this.volume * 0.7) 37 | 38 | this.nextUpdateFrame -= samples.length 39 | if (this.nextUpdateFrame < 0) { 40 | this.nextUpdateFrame += this.intervalInFrames 41 | this.port.postMessage({volume: this.volume}) 42 | } 43 | } 44 | 45 | return true 46 | } 47 | }`; 48 | 49 | export default VolMeterWorket; 50 | -------------------------------------------------------------------------------- /src/types/agent.types.ts: -------------------------------------------------------------------------------- 1 | import { Tool, Resource } from "@modelcontextprotocol/sdk/types.js"; 2 | import { LiveConfig } from "./multimodal-live-types"; 3 | 4 | export interface PromptPost { 5 | instruction: { 6 | static: string; 7 | state: string; 8 | dynamic: string; 9 | }; 10 | input: { 11 | name: string; 12 | description: string; 13 | type: string[]; 14 | reference: string[]; 15 | }; 16 | output: { 17 | name: string; 18 | description: string; 19 | type: string[]; 20 | reference: string[]; 21 | }; 22 | metadata: { 23 | title: string; 24 | description: string; 25 | tag: string[]; 26 | log_message: string; 27 | }; 28 | } 29 | 30 | export interface AgentConfig { 31 | id: string; 32 | name: string; 33 | description: string; 34 | instruction: string; 35 | tools: Array<{ 36 | name: string; 37 | description: string; 38 | parameters: Record; 39 | }>; 40 | resources: Resource[]; 41 | _source: "user" | "system"; 42 | } 43 | 44 | export interface AgentRegistryContextType { 45 | agents: AgentConfig[]; 46 | activeAgent: string | null; 47 | setActiveAgent: (agentName: string | null) => void; 48 | tools: Tool[]; 49 | resources: Resource[]; 50 | activeTools: Tool[]; 51 | activeResources: Resource[]; 52 | toggleTool: (tool: Tool) => void; 53 | toggleResource: (resource: Resource) => void; 54 | config: LiveConfig; 55 | } 56 | -------------------------------------------------------------------------------- /src/features/llm-registry/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { PromptMessage } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | export interface LlmProviderConfig { 4 | id: string; 5 | name: string; 6 | description: string; 7 | configSchema: { 8 | [key: string]: { 9 | type: "string" | "number" | "boolean" | "select"; 10 | label: string; 11 | description?: string; 12 | default?: unknown; 13 | options?: Array<{ label: string; value: string }>; 14 | isSecret?: boolean; 15 | }; 16 | }; 17 | } 18 | 19 | export interface LlmProviderInstance { 20 | id: string; 21 | name: string; 22 | executePrompt: (prompt: { 23 | name: string; 24 | messages: PromptMessage[]; 25 | params?: Record; 26 | }) => Promise; 27 | isLoading: boolean; 28 | error: string | null; 29 | } 30 | 31 | export interface LlmRegistryContextType { 32 | providers: LlmProviderConfig[]; 33 | activeProvider: string | null; 34 | providerConfig: Record; 35 | getProviderConfig: (providerId: string) => LlmProviderConfig | null; 36 | getProviderInstance: (providerId: string) => LlmProviderInstance | null; 37 | registerProvider: ( 38 | config: LlmProviderConfig, 39 | instance: LlmProviderInstance 40 | ) => void; 41 | unregisterProvider: (providerId: string) => void; 42 | } 43 | 44 | export interface LlmConfig { 45 | provider: string; 46 | config: Record; 47 | } 48 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/server/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export type ModalMode = "view" | "execute" | null; 4 | 5 | export interface UseModalReturn { 6 | viewModalOpen: boolean; 7 | executeModalOpen: boolean; 8 | selectedPrompt: T | null; 9 | handleOpenViewModal: (item: T) => void; 10 | handleOpenExecuteModal: (item: T) => void; 11 | handleCloseViewModal: () => void; 12 | handleCloseExecuteModal: () => void; 13 | } 14 | 15 | export function useModal(): UseModalReturn { 16 | const [viewModalOpen, setViewModalOpen] = useState(false); 17 | const [executeModalOpen, setExecuteModalOpen] = useState(false); 18 | const [selectedPrompt, setSelectedPrompt] = useState(null); 19 | 20 | const handleOpenViewModal = (item: T) => { 21 | setSelectedPrompt(item); 22 | setViewModalOpen(true); 23 | }; 24 | 25 | const handleOpenExecuteModal = (item: T) => { 26 | setSelectedPrompt(item); 27 | setExecuteModalOpen(true); 28 | }; 29 | 30 | const handleCloseViewModal = () => { 31 | setViewModalOpen(false); 32 | setSelectedPrompt(null); 33 | }; 34 | 35 | const handleCloseExecuteModal = () => { 36 | setExecuteModalOpen(false); 37 | setSelectedPrompt(null); 38 | }; 39 | 40 | return { 41 | viewModalOpen, 42 | executeModalOpen, 43 | selectedPrompt, 44 | handleOpenViewModal, 45 | handleOpenExecuteModal, 46 | handleCloseViewModal, 47 | handleCloseExecuteModal, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/features/server/utils/prompt-utils.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema7 } from "json-schema"; 2 | import type { 3 | PromptMessage, 4 | JSONRPCError, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | import { LogType } from "@/stores/log-store"; 7 | import Ajv from "ajv"; 8 | 9 | const ajv = new Ajv(); 10 | 11 | export interface ValidationError { 12 | path: string[]; 13 | message: string; 14 | } 15 | 16 | export interface PromptLogEntry { 17 | type: LogType; 18 | operation: string; 19 | status: "success" | "error"; 20 | name: string; 21 | params?: Record; 22 | result?: PromptMessage[]; 23 | error?: string; 24 | } 25 | 26 | export function getErrorMessage(error: unknown): string { 27 | if (error instanceof Error) return error.message; 28 | if (typeof error === "object" && error !== null && "error" in error) { 29 | const rpcError = error as JSONRPCError; 30 | return rpcError.error.message; 31 | } 32 | return "An unexpected error occurred"; 33 | } 34 | 35 | export function validatePromptParameters( 36 | schema: JSONSchema7, 37 | values: Record 38 | ): ValidationError[] { 39 | const validate = ajv.compile(schema); 40 | const isValid = validate(values); 41 | 42 | if (!isValid && validate.errors) { 43 | return validate.errors.map((err) => ({ 44 | path: err.schemaPath.split("/").filter(Boolean), 45 | message: err.message || "Invalid value", 46 | })); 47 | } 48 | 49 | return []; 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/** 3 | /node_modules 4 | proxy/node_modules/** 5 | **/node_modules 6 | 7 | # Build output 8 | build/ 9 | dist/ 10 | /dist 11 | **/dist 12 | /build 13 | **/build 14 | 15 | # TypeScript build info 16 | *.tsbuildinfo 17 | **/*.tsbuildinfo 18 | .tsbuildinfo 19 | build/**/*.tsbuildinfo 20 | 21 | # Configuration files - ignore everything in these directories first 22 | /config/* 23 | /extensions/* 24 | 25 | # Scripts - ignore specific directories 26 | /scripts/private/* 27 | 28 | # Other directories to ignore 29 | /coverage/* 30 | /agent/* 31 | /credentials/* 32 | 33 | # Google auth credentials (but not the script itself) 34 | /scripts/google-auth/credentials/* 35 | /scripts/google-auth/node_modules/* 36 | 37 | # But don't ignore these specific files 38 | !/credentials/.gitkeep 39 | !/config/README.md 40 | !/config/*.example.ts 41 | !/config/*.example.json 42 | !/config/*server.config.ts 43 | !/config/types.ts 44 | !/extensions/README.md 45 | 46 | # Don't ignore google-auth script files 47 | !/scripts/google-auth 48 | !/scripts/google-auth/auth-google.ts 49 | !/scripts/google-auth/package.json 50 | !/scripts/google-auth/tsconfig.json 51 | !/scripts/google-auth/README.md 52 | !/scripts/google-auth/credentials/.gitkeep 53 | 54 | # Environment variables 55 | .env 56 | .env.local 57 | .env.*.local 58 | 59 | # IDE specific files 60 | .idea/ 61 | .vscode/ 62 | *.swp 63 | *.swo 64 | 65 | # Output 66 | output/ 67 | 68 | # Agent 69 | agent/ 70 | 71 | # Coverage 72 | coverage/ 73 | -------------------------------------------------------------------------------- /src/components/sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarItem } from "./SidebarItem"; 2 | import { SidebarSection } from "./types"; 3 | import { cn } from "@nextui-org/react"; 4 | 5 | interface SidebarProps { 6 | isCompact?: boolean; 7 | items: SidebarSection[]; 8 | defaultSelectedKey?: string; 9 | onItemClick?: (href: string | undefined) => void; 10 | } 11 | 12 | export default function Sidebar({ 13 | isCompact = false, 14 | items, 15 | defaultSelectedKey, 16 | onItemClick, 17 | }: SidebarProps) { 18 | return ( 19 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/hooks/useMcpSampling.types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CreateMessageRequest, 3 | CreateMessageResult, 4 | ProgressToken, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | 7 | export interface SamplingProgress { 8 | progressToken: ProgressToken; 9 | progress: number; 10 | total: number; 11 | status?: string; 12 | } 13 | 14 | export interface PendingSampleRequest { 15 | id: number; 16 | serverId: string; 17 | request: CreateMessageRequest["params"]; 18 | resolve: (result: CreateMessageResult) => void; 19 | reject: (error: Error) => void; 20 | progress?: (status: string) => void; 21 | } 22 | 23 | export interface SamplingHookState { 24 | pendingSampleRequests: PendingSampleRequest[]; 25 | } 26 | 27 | export interface SamplingHookActions { 28 | requestSampling: ( 29 | serverId: string, 30 | request: CreateMessageRequest["params"], 31 | progress?: (status: string) => void 32 | ) => Promise; 33 | handleApproveSampling: ( 34 | id: number, 35 | response: CreateMessageResult 36 | ) => Promise; 37 | handleRejectSampling: (id: number) => void; 38 | } 39 | 40 | export type SamplingHookReturn = SamplingHookState & SamplingHookActions; 41 | 42 | export interface SamplingErrorType extends Error { 43 | code: string; 44 | } 45 | 46 | export const createSamplingError = ( 47 | message: string, 48 | code: string 49 | ): SamplingErrorType => { 50 | const error = new Error(message) as SamplingErrorType; 51 | error.name = "SamplingError"; 52 | error.code = code; 53 | return error; 54 | }; 55 | -------------------------------------------------------------------------------- /docs/statusindicator-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # StatusIndicator Component 2 | 3 | ## Overview 4 | 5 | The StatusIndicator component provides a visual representation of various states through colored indicators. It's commonly used to show status, progress, or availability in the interface. 6 | 7 | ## Available Variants 8 | 9 | - Dot: A simple circular indicator 10 | - Pulse: An animated pulsing indicator 11 | - Ring: A circular outline indicator 12 | 13 | ## Props and Configuration 14 | 15 | | Prop | Type | Default | Description | 16 | | ------- | ---------------------------------------------- | --------- | --------------------------------- | 17 | | variant | 'dot' \| 'pulse' \| 'ring' | 'dot' | The visual style of the indicator | 18 | | status | 'success' \| 'warning' \| 'error' \| 'neutral' | 'neutral' | The status to display | 19 | | size | 'sm' \| 'md' \| 'lg' | 'md' | The size of the indicator | 20 | 21 | ## Usage Examples 22 | 23 | ```jsx 24 | // Basic usage 25 | 26 | 27 | // With variant 28 | 29 | 30 | // With custom size 31 | 32 | ``` 33 | 34 | ## Testing Guidelines 35 | 36 | 1. Test all variant combinations 37 | 2. Verify color mappings for each status 38 | 3. Check accessibility requirements 39 | 4. Validate size adjustments 40 | 5. Test animation behaviors for pulse variant 41 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/ToolCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import "@testing-library/jest-dom"; 4 | import { ToolCard } from "../ToolCard"; 5 | 6 | // Mock the Icon component 7 | vi.mock("@iconify/react", () => ({ 8 | Icon: ({ 9 | icon, 10 | className, 11 | "data-testid": testId, 12 | }: { 13 | icon: string; 14 | className?: string; 15 | "data-testid"?: string; 16 | }) => ( 17 |
18 | {icon} 19 |
20 | ), 21 | })); 22 | 23 | describe("ToolCard", () => { 24 | const defaultProps = { 25 | name: "Test Tool", 26 | description: "Test Description", 27 | }; 28 | 29 | it("renders with default props", () => { 30 | render(); 31 | expect(screen.getByText("Test Tool")).toBeInTheDocument(); 32 | expect(screen.getByText("Test Description")).toBeInTheDocument(); 33 | expect(screen.getByText("Tool")).toBeInTheDocument(); 34 | }); 35 | 36 | it("uses custom tool type", () => { 37 | const customType = "Custom Type"; 38 | render(); 39 | expect(screen.getByText(customType)).toBeInTheDocument(); 40 | }); 41 | 42 | it("renders children content", () => { 43 | render( 44 | 45 |
Child Content
46 |
47 | ); 48 | expect(screen.getByText("Child Content")).toBeInTheDocument(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/Link/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "@nextui-org/react"; 3 | import { Icon } from "@iconify/react"; 4 | import { Tooltip } from "@nextui-org/react"; 5 | 6 | interface ExternalLinkProps { 7 | href: string; 8 | type: "github" | "npm"; 9 | className?: string; 10 | showIcon?: boolean; 11 | showLabel?: boolean; 12 | size?: "sm" | "md" | "lg"; 13 | } 14 | 15 | const LINK_CONFIGS = { 16 | github: { 17 | label: "GitHub", 18 | icon: "solar:github-circle-bold-duotone", 19 | tooltip: "View on GitHub", 20 | }, 21 | npm: { 22 | label: "NPM", 23 | icon: "solar:box-bold-duotone", 24 | tooltip: "View on NPM", 25 | }, 26 | }; 27 | 28 | /** 29 | * ExternalLink component that provides consistent styling for external links (GitHub, NPM) 30 | */ 31 | export function ExternalLink({ 32 | href, 33 | type, 34 | className = "", 35 | showIcon = true, 36 | showLabel = true, 37 | size = "sm", 38 | }: ExternalLinkProps) { 39 | const config = LINK_CONFIGS[type]; 40 | 41 | const content = ( 42 | 49 | {showIcon && } 50 | {showLabel && {config.label}} 51 | 52 | ); 53 | 54 | return {content}; 55 | } 56 | -------------------------------------------------------------------------------- /src/components/StatusIndicator/ServerConnectionStatus.tsx: -------------------------------------------------------------------------------- 1 | import { Chip } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | 4 | type ConnectionStatus = "connected" | "pending" | "error" | "disconnected"; 5 | 6 | interface ServerConnectionStatusProps { 7 | status: ConnectionStatus; 8 | className?: string; 9 | } 10 | 11 | const statusConfig: Record< 12 | ConnectionStatus, 13 | { 14 | label: string; 15 | color: "success" | "warning" | "danger" | "default"; 16 | icon: string; 17 | } 18 | > = { 19 | connected: { 20 | label: "Connected", 21 | color: "success", 22 | icon: "solar:check-circle-bold-duotone", 23 | }, 24 | pending: { 25 | label: "Connecting", 26 | color: "warning", 27 | icon: "solar:loading-bold-duotone", 28 | }, 29 | error: { 30 | label: "Error", 31 | color: "danger", 32 | icon: "solar:shield-warning-bold-duotone", 33 | }, 34 | disconnected: { 35 | label: "Disconnected", 36 | color: "default", 37 | icon: "solar:plug-circle-bold-duotone", 38 | }, 39 | }; 40 | 41 | export function ServerConnectionStatus({ 42 | status, 43 | className = "", 44 | }: ServerConnectionStatusProps) { 45 | const config = statusConfig[status]; 46 | 47 | return ( 48 | 57 | } 58 | className={className} 59 | > 60 | {config.label} 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/hooks/useMcpSampling.utils.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import type { ProgressToken } from "@modelcontextprotocol/sdk/types.js"; 3 | 4 | export class SamplingProgressManager { 5 | constructor(private client: Client, private progressToken: ProgressToken) {} 6 | 7 | async sendProgress(params: { 8 | progress: number; 9 | total?: number; 10 | status?: string; 11 | }): Promise { 12 | await this.client.notification({ 13 | method: "notifications/progress", 14 | params: { 15 | progressToken: this.progressToken, 16 | progress: params.progress, 17 | total: params.total ?? 100, 18 | ...(params.status && { status: params.status }), 19 | }, 20 | }); 21 | } 22 | 23 | async start(): Promise { 24 | await this.sendProgress({ 25 | progress: 0, 26 | status: "Starting sampling process...", 27 | }); 28 | } 29 | 30 | async update(progress: number): Promise { 31 | await this.sendProgress({ 32 | progress, 33 | status: "Processing...", 34 | }); 35 | } 36 | 37 | async complete(): Promise { 38 | await this.sendProgress({ 39 | progress: 100, 40 | status: "Sampling complete", 41 | }); 42 | } 43 | 44 | async reject(): Promise { 45 | await this.sendProgress({ 46 | progress: 0, 47 | status: "Sampling rejected", 48 | }); 49 | } 50 | 51 | async error(message: string): Promise { 52 | await this.sendProgress({ 53 | progress: 0, 54 | status: `Error: ${message}`, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Layout/CollapsibleSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { ChevronDown, ChevronUp } from "lucide-react"; 3 | 4 | interface CollapsibleSectionProps { 5 | title: string; 6 | titleContent?: React.ReactNode; 7 | children: React.ReactNode; 8 | defaultExpanded?: boolean; 9 | isExpanded?: boolean; 10 | onExpandedChange?: (expanded: boolean) => void; 11 | } 12 | 13 | export const CollapsibleSection: React.FC = ({ 14 | title, 15 | titleContent, 16 | children, 17 | defaultExpanded = true, 18 | isExpanded: controlledIsExpanded, 19 | onExpandedChange, 20 | }) => { 21 | const [internalIsExpanded, setInternalIsExpanded] = useState(defaultExpanded); 22 | 23 | const isExpanded = controlledIsExpanded ?? internalIsExpanded; 24 | const setIsExpanded = (expanded: boolean) => { 25 | setInternalIsExpanded(expanded); 26 | onExpandedChange?.(expanded); 27 | }; 28 | 29 | return ( 30 |
31 | 39 |
43 | {children} 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default CollapsibleSection; 50 | -------------------------------------------------------------------------------- /src/contexts/LlmProviderContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, ReactNode } from "react"; 2 | import { PromptMessage } from "@modelcontextprotocol/sdk/types.js"; 3 | import { useLlmRegistry } from "@/features/llm-registry"; 4 | import { McpMeta } from "@/types/mcp"; 5 | 6 | interface LlmProviderContextType { 7 | executePrompt: (prompt: { 8 | name: string; 9 | messages: PromptMessage[]; 10 | params?: Record; 11 | _meta?: McpMeta; 12 | }) => Promise; 13 | isLoading: boolean; 14 | error: string | null; 15 | } 16 | 17 | const defaultContext: LlmProviderContextType = { 18 | executePrompt: async () => { 19 | throw new Error("No LLM provider is active"); 20 | }, 21 | isLoading: false, 22 | error: null, 23 | }; 24 | 25 | const LlmProviderContext = 26 | createContext(defaultContext); 27 | 28 | interface Props { 29 | children: ReactNode; 30 | } 31 | 32 | export function GlobalLlmProvider({ children }: Props) { 33 | const { activeProvider, getProviderInstance } = useLlmRegistry(); 34 | 35 | const instance = activeProvider ? getProviderInstance(activeProvider) : null; 36 | 37 | return ( 38 | 49 | {children} 50 | 51 | ); 52 | } 53 | 54 | export function useGlobalLlm() { 55 | return useContext(LlmProviderContext); 56 | } 57 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/lib/worklets/audio-processing.ts: -------------------------------------------------------------------------------- 1 | const AudioRecordingWorklet = ` 2 | class AudioProcessingWorklet extends AudioWorkletProcessor { 3 | 4 | // send and clear buffer every 2048 samples, 5 | // which at 16khz is about 8 times a second 6 | buffer = new Int16Array(2048); 7 | 8 | // current write index 9 | bufferWriteIndex = 0; 10 | 11 | constructor() { 12 | super(); 13 | this.hasAudio = false; 14 | } 15 | 16 | /** 17 | * @param inputs Float32Array[][] [input#][channel#][sample#] so to access first inputs 1st channel inputs[0][0] 18 | * @param outputs Float32Array[][] 19 | */ 20 | process(inputs) { 21 | if (inputs[0].length) { 22 | const channel0 = inputs[0][0]; 23 | this.processChunk(channel0); 24 | } 25 | return true; 26 | } 27 | 28 | sendAndClearBuffer(){ 29 | this.port.postMessage({ 30 | event: "chunk", 31 | data: { 32 | int16arrayBuffer: this.buffer.slice(0, this.bufferWriteIndex).buffer, 33 | }, 34 | }); 35 | this.bufferWriteIndex = 0; 36 | } 37 | 38 | processChunk(float32Array) { 39 | const l = float32Array.length; 40 | 41 | for (let i = 0; i < l; i++) { 42 | // convert float32 -1 to 1 to int16 -32768 to 32767 43 | const int16Value = float32Array[i] * 32768; 44 | this.buffer[this.bufferWriteIndex++] = int16Value; 45 | if(this.bufferWriteIndex >= this.buffer.length) { 46 | this.sendAndClearBuffer(); 47 | } 48 | } 49 | 50 | if(this.bufferWriteIndex >= this.buffer.length) { 51 | this.sendAndClearBuffer(); 52 | } 53 | } 54 | } 55 | `; 56 | 57 | export default AudioRecordingWorklet; 58 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Zepto"; 7 | src: url("/font/Zepto.woff") format("woff"); 8 | font-weight: normal; 9 | font-style: normal; 10 | font-display: swap; 11 | } 12 | 13 | :root { 14 | --foreground-rgb: 0, 0, 0; 15 | --background-start-rgb: 74, 74, 80; /* #4a4a50 */ 16 | --background-end-rgb: 63, 63, 70; /* #3f3f46 */ 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | :root { 21 | --foreground-rgb: 255, 255, 255; 22 | --background-start-rgb: 74, 74, 80; /* #4a4a50 */ 23 | --background-end-rgb: 63, 63, 70; /* #3f3f46 */ 24 | } 25 | } 26 | 27 | @layer utilities { 28 | .text-balance { 29 | text-wrap: balance; 30 | } 31 | } 32 | 33 | h1, 34 | h2, 35 | h3, 36 | h4, 37 | h5, 38 | h6 { 39 | font-family: "Zepto"; 40 | text-transform: uppercase; 41 | font-weight: 400; 42 | } 43 | 44 | p, 45 | span, 46 | button { 47 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", 48 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 49 | } 50 | 51 | /* For WebKit browsers */ 52 | 53 | ::-webkit-scrollbar { 54 | width: 20px; /* Thinner scrollbar */ 55 | } 56 | 57 | ::-webkit-scrollbar-track { 58 | background: black; /* Black track */ 59 | } 60 | 61 | ::-webkit-scrollbar-thumb { 62 | background: white; /* White thumb */ 63 | } 64 | 65 | /* For Firefox */ 66 | * { 67 | scrollbar-width: thin; /* Thinner scrollbar */ 68 | scrollbar-color: white black; /* White thumb and black track */ 69 | } 70 | 71 | @media (max-width: 640px) { 72 | .scale-sm { 73 | transform: scale(0.7); 74 | transform-origin: left; 75 | } 76 | } 77 | 78 | .format-icon path { 79 | fill: white; 80 | } 81 | -------------------------------------------------------------------------------- /src/contexts/api.ts: -------------------------------------------------------------------------------- 1 | import { GoogleGenerativeAI } from "@google/generative-ai"; 2 | import { PromptMessage } from "@modelcontextprotocol/sdk/types.js"; 3 | 4 | const GEMINI_API_KEY = import.meta.env.VITE_GEMINI_API_KEY; 5 | const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); 6 | 7 | export async function generateLlmResponse( 8 | messages: PromptMessage[] 9 | ): Promise<{ response: string; error?: string }> { 10 | try { 11 | const model = genAI.getGenerativeModel({ model: "gemini-pro" }); 12 | 13 | // Format messages into a clear prompt structure 14 | const prompt = messages 15 | .map((msg) => { 16 | let content = ""; 17 | if (msg.content.type === "text" && msg.content.text) { 18 | content = msg.content.text; 19 | } else if (msg.content.type === "resource" && msg.content.resource) { 20 | content = `Resource ${msg.content.resource.uri}:\n${msg.content.resource.text}`; 21 | } 22 | return content ? `${msg.role.toUpperCase()}: ${content}` : ""; 23 | }) 24 | .filter(Boolean) 25 | .join("\n\n"); 26 | 27 | if (!prompt) { 28 | throw new Error("No valid content found in messages"); 29 | } 30 | 31 | // Send the prompt and get response 32 | const result = await model.generateContent(prompt); 33 | const response = await result.response; 34 | const text = response.text().trim(); 35 | 36 | if (!text) { 37 | throw new Error("No response received from Gemini"); 38 | } 39 | 40 | return { 41 | response: text, 42 | }; 43 | } catch (error) { 44 | console.error("Gemini API error:", error); 45 | return { 46 | response: "", 47 | error: error instanceof Error ? error.message : "An error occurred", 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/components/audio-pulse/AudioPulse.tsx: -------------------------------------------------------------------------------- 1 | import cn from "classnames"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | const lineCount = 3; 5 | 6 | export type AudioPulseProps = { 7 | active: boolean; 8 | volume: number; 9 | hover?: boolean; 10 | }; 11 | 12 | export default function AudioPulse({ active, volume, hover }: AudioPulseProps) { 13 | const lines = useRef([]); 14 | 15 | useEffect(() => { 16 | let timeout: number | null = null; 17 | const update = () => { 18 | lines.current.forEach( 19 | (line, i) => 20 | (line.style.height = `${Math.min( 21 | 24, 22 | 4 + volume * (i === 1 ? 400 : 60) 23 | )}px`) 24 | ); 25 | timeout = window.setTimeout(update, 100); 26 | }; 27 | 28 | update(); 29 | return () => clearTimeout((timeout as number)!); 30 | }, [volume]); 31 | 32 | return ( 33 |
39 | {Array(lineCount) 40 | .fill(null) 41 | .map((_, i) => ( 42 |
{ 45 | if (el) lines.current[i] = el; 46 | }} 47 | className={cn( 48 | "w-1.5 min-h-1.5 rounded-full transition-all duration-200 ease-out transform-gpu", 49 | active ? "bg-default-800 animate-pulse" : "bg-default-300", 50 | hover && "animate-hover" 51 | )} 52 | style={{ 53 | animationDelay: `${i * 133}ms`, 54 | }} 55 | /> 56 | ))} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/features/server/__tests__/PromptCard.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { describe, it, expect, vi } from "vitest"; 3 | import { render, screen, fireEvent } from "@testing-library/react"; 4 | import { PromptCard } from "../../../components/Card/PromptCard"; 5 | 6 | describe("PromptCard", () => { 7 | const mockPrompt = { 8 | name: "Test Prompt", 9 | description: "A test prompt", 10 | type: "test", 11 | }; 12 | 13 | const defaultProps = { 14 | prompt: mockPrompt, 15 | onExecute: vi.fn(), 16 | onView: vi.fn(), 17 | isLoading: false, 18 | }; 19 | 20 | it("renders prompt information correctly", () => { 21 | render(); 22 | expect(screen.getByText("Test Prompt")).toBeInTheDocument(); 23 | expect(screen.getByText("A test prompt")).toBeInTheDocument(); 24 | expect(screen.getByText("Prompt")).toBeInTheDocument(); 25 | }); 26 | 27 | it("calls onExecute when execute button is clicked", () => { 28 | render(); 29 | const executeButton = screen.getByTestId("prompt-execute-Test Prompt"); 30 | fireEvent.click(executeButton); 31 | expect(defaultProps.onExecute).toHaveBeenCalled(); 32 | }); 33 | 34 | it("calls onView when view button is clicked", () => { 35 | render(); 36 | const viewButton = screen.getByRole("button", { name: /View Prompt/i }); 37 | fireEvent.click(viewButton); 38 | expect(defaultProps.onView).toHaveBeenCalled(); 39 | }); 40 | 41 | it("disables execute button when loading", () => { 42 | render(); 43 | const executeButton = screen.getByTestId("prompt-execute-Test Prompt"); 44 | expect(executeButton).toBeDisabled(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /config/agent.config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome-to-systemprompt": { 3 | "name": "Welcome to Systemprompt", 4 | "description": "The default example agent that introduces you to Systemprompt and helps you get started", 5 | "instruction": "You are the Welcome Agent for Systemprompt, designed to help users understand and get started with the Systemprompt platform. Your role is to be friendly, informative, and helpful in guiding users through their first steps.\n\n1. Initial Greeting:\n- Always start with a warm welcome to Systemprompt\n- Introduce yourself as the Welcome Agent\n- Explain that you're here to help them understand and use Systemprompt effectively\n\n2. Core Concepts:\n- Explain that Systemprompt is a platform for managing and executing system prompts\n- Describe how system prompts work as instructions for AI models\n- Highlight the importance of well-structured prompts for consistent AI behavior\n\n3. Key Features:\n- Outline the main features of Systemprompt:\n * Creating and editing system prompts\n * Managing multiple prompt configurations\n * Testing prompts with different models\n * Organizing prompts in collections\n\n4. Best Practices:\n- Share tips for creating effective system prompts\n- Explain the importance of clear, specific instructions\n- Suggest ways to test and iterate on prompts\n\n5. Getting Started:\n- Guide users through their first steps\n- Recommend starting with simple prompts\n- Explain how to use the interface and tools\n\n6. Support:\n- Offer to answer any questions about Systemprompt\n- Direct users to documentation and resources\n- Encourage experimentation and learning\n\nRemember: Keep your responses friendly, clear, and focused on helping users understand and get value from Systemprompt. Always be patient and supportive as users learn the platform." 6 | } 7 | } -------------------------------------------------------------------------------- /src/features/multimodal-agent/__tests__/contexts/McpContext.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { McpProvider } from "../../../../contexts/McpProvider"; 4 | import { useContext } from "react"; 5 | import { McpContext } from "../../../../contexts/McpContext"; 6 | import React from "react"; 7 | 8 | const TestComponent = () => { 9 | const context = useContext(McpContext); 10 | if (!context) return null; 11 | return ( 12 |
13 |
Active Clients: {context.activeClients.join(", ")}
14 |
15 | Connection Status: {context.clients.testServer?.connectionStatus} 16 |
17 |
18 | ); 19 | }; 20 | 21 | // Mock the hooks used by McpProvider 22 | vi.mock("../../../../hooks/useMcpClient", () => ({ 23 | useMcpClient: () => ({ 24 | clients: {}, 25 | activeClients: [], 26 | updateClientState: vi.fn(), 27 | setupClientNotifications: vi.fn(), 28 | }), 29 | })); 30 | 31 | vi.mock("../../../../hooks/useMcpConnection", () => ({ 32 | useMcpConnection: () => ({ 33 | connectServer: vi.fn(), 34 | disconnectServer: vi.fn(), 35 | }), 36 | })); 37 | 38 | describe("McpProvider", () => { 39 | it("renders children", () => { 40 | render( 41 | 42 |
Test Child
43 |
44 | ); 45 | 46 | expect(screen.getByText("Test Child")).toBeInTheDocument(); 47 | }); 48 | 49 | it("initializes with empty state", () => { 50 | render( 51 | 52 | 53 | 54 | ); 55 | 56 | expect(screen.getByText(/^Active Clients:$/)).toBeInTheDocument(); 57 | expect(screen.getByText(/^Connection Status:$/)).toBeInTheDocument(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /proxy/dist/mcpProxy.js: -------------------------------------------------------------------------------- 1 | import { McpHandlers } from "./handlers/mcpHandlers.js"; 2 | import { ConfigHandlers } from "./handlers/configHandlers.js"; 3 | import { TransportHandlers } from "./handlers/transportHandlers.js"; 4 | import { defaults } from "./config/defaults.js"; 5 | export default function mcpProxy({ transportToClient, transportToServer, onerror, }) { 6 | let transportToClientClosed = false; 7 | let transportToServerClosed = false; 8 | transportToClient.onmessage = (message) => { 9 | transportToServer.send(message).catch(onerror); 10 | }; 11 | transportToServer.onmessage = (message) => { 12 | transportToClient.send(message).catch(onerror); 13 | }; 14 | transportToClient.onclose = () => { 15 | if (transportToServerClosed) { 16 | return; 17 | } 18 | transportToClientClosed = true; 19 | transportToServer.close().catch(onerror); 20 | }; 21 | transportToServer.onclose = () => { 22 | if (transportToClientClosed) { 23 | return; 24 | } 25 | transportToServerClosed = true; 26 | transportToClient.close().catch(onerror); 27 | }; 28 | transportToClient.onerror = onerror; 29 | transportToServer.onerror = onerror; 30 | } 31 | export class McpProxy { 32 | constructor(config) { 33 | // Ensure all required properties are initialized 34 | const initializedConfig = { 35 | mcpServers: config.mcpServers || {}, 36 | available: config.available || {}, 37 | defaults: config.defaults || defaults, 38 | }; 39 | this.mcpHandlers = new McpHandlers(initializedConfig); 40 | this.configHandlers = new ConfigHandlers(initializedConfig); 41 | this.transportHandlers = new TransportHandlers(initializedConfig); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/features/server/hooks/usePromptLogger.ts: -------------------------------------------------------------------------------- 1 | import { useLogStore } from "@/stores/log-store"; 2 | 3 | type OperationType = "View Prompt" | "Execute Prompt"; 4 | 5 | interface LogEntry { 6 | type: "prompt"; 7 | operation: OperationType; 8 | status: "success" | "error"; 9 | name: string; 10 | params?: Record; 11 | result?: unknown; 12 | error?: string; 13 | } 14 | 15 | interface UsePromptLoggerReturn { 16 | log: (entry: Omit) => void; 17 | logSuccess: ( 18 | operation: OperationType, 19 | name: string, 20 | params?: Record, 21 | result?: unknown 22 | ) => void; 23 | logError: ( 24 | operation: OperationType, 25 | name: string, 26 | error: Error | string, 27 | params?: Record 28 | ) => void; 29 | } 30 | 31 | export function usePromptLogger(): UsePromptLoggerReturn { 32 | const { addLog } = useLogStore(); 33 | 34 | const log = (entry: Omit) => { 35 | addLog({ 36 | type: "prompt", 37 | ...entry, 38 | }); 39 | }; 40 | 41 | const logSuccess = ( 42 | operation: OperationType, 43 | name: string, 44 | params?: Record, 45 | result?: unknown 46 | ) => { 47 | log({ 48 | operation, 49 | status: "success", 50 | name, 51 | params, 52 | result, 53 | }); 54 | }; 55 | 56 | const logError = ( 57 | operation: OperationType, 58 | name: string, 59 | error: Error | string, 60 | params?: Record 61 | ) => { 62 | log({ 63 | operation, 64 | status: "error", 65 | name, 66 | params, 67 | error: error instanceof Error ? error.message : error, 68 | }); 69 | }; 70 | 71 | return { 72 | log, 73 | logSuccess, 74 | logError, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/features/server/components/sections/ServerHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | 4 | interface ServerHeaderProps { 5 | serverName: string; 6 | serverId: string; 7 | isConnected: boolean; 8 | isConnecting: boolean; 9 | hasError: boolean; 10 | onConnect: () => void; 11 | onDisconnect: () => void; 12 | icon?: string; 13 | } 14 | 15 | export function ServerHeader({ 16 | serverName, 17 | serverId, 18 | isConnected, 19 | isConnecting, 20 | icon = "solar:server-bold-duotone", 21 | onConnect, 22 | onDisconnect, 23 | }: ServerHeaderProps) { 24 | return ( 25 |
26 |
27 | 28 |
29 |

{serverName}

30 |

{serverId}

31 |
32 |
33 |
34 | {isConnected ? ( 35 | 44 | ) : ( 45 | 57 | )} 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /proxy/src/types/server.types.ts: -------------------------------------------------------------------------------- 1 | import { McpModuleMetadata } from "./index.js"; 2 | import { SystempromptModule } from "./systemprompt.js"; 3 | 4 | export interface ServerMetadata { 5 | icon?: string; 6 | color?: string; 7 | description?: string; 8 | serverType?: "core" | "custom"; 9 | name?: string; 10 | } 11 | 12 | export interface ServerConfig { 13 | id: string; 14 | command: string; 15 | args: string[]; 16 | env?: string[]; 17 | metadata?: ServerMetadata; 18 | } 19 | 20 | export interface ServerDefaults { 21 | serverTypes: { 22 | stdio: ServerMetadata; 23 | sse: ServerMetadata; 24 | }; 25 | unconnected: ServerMetadata; 26 | } 27 | 28 | export const DEFAULT_SERVER_CONFIG: ServerDefaults = { 29 | serverTypes: { 30 | stdio: { 31 | icon: "solar:server-minimalistic-bold-duotone", 32 | color: "primary", 33 | description: "Local stdio-based MCP server", 34 | }, 35 | sse: { 36 | icon: "solar:server-square-cloud-bold-duotone", 37 | color: "primary", 38 | description: "Remote SSE-based MCP server", 39 | }, 40 | }, 41 | unconnected: { 42 | icon: "solar:server-broken", 43 | color: "secondary", 44 | description: "Remote MCP server (not connected)", 45 | }, 46 | }; 47 | 48 | export interface ServerResponse { 49 | mcpServers: Record; 50 | customServers: Record; 51 | available: Record; 52 | defaults: ServerDefaults; 53 | } 54 | 55 | export interface McpServer extends ServerConfig { 56 | id: string; 57 | type: string; 58 | title: string; 59 | description: string; 60 | metadata: ServerMetadata & Partial; 61 | } 62 | 63 | export interface McpDataInterface { 64 | mcpServers: Record; 65 | defaults: ServerDefaults; 66 | available: Record; 67 | } 68 | -------------------------------------------------------------------------------- /src/components/shared/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button as NextUIButton, ButtonProps } from "@nextui-org/react"; 2 | 3 | interface SharedButtonProps extends ButtonProps { 4 | isLoading?: boolean; 5 | } 6 | 7 | export function Button({ 8 | children, 9 | isLoading, 10 | disabled, 11 | className = "", 12 | ...props 13 | }: SharedButtonProps) { 14 | const isDisabled = disabled || isLoading; 15 | 16 | return ( 17 | 25 | {isLoading ? ( 26 |
27 | 48 | Loading 49 |
50 | ) : null} 51 | 56 | {children} 57 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip, cn } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | import { SidebarItem as SidebarItemType } from "./types"; 4 | 5 | const colorMap = { 6 | success: "#17c964", 7 | warning: "#f5a524", 8 | primary: "#f6933c", 9 | secondary: "#7e868c", 10 | } as const; 11 | 12 | interface SidebarItemProps { 13 | item: SidebarItemType; 14 | isCompact?: boolean; 15 | isSelected?: boolean; 16 | onPress?: () => void; 17 | } 18 | 19 | export function SidebarItem({ 20 | item, 21 | isCompact = false, 22 | isSelected = false, 23 | onPress, 24 | }: SidebarItemProps) { 25 | if (!item.icon) { 26 | console.warn("No icon provided for item:", item); 27 | } 28 | 29 | const buttonContent = ( 30 | 54 | ); 55 | 56 | if (isCompact && item.description) { 57 | return ( 58 | 59 | {buttonContent} 60 | 61 | ); 62 | } 63 | 64 | return buttonContent; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Button/BaseButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button as NextUIButton, 3 | ButtonProps as NextUIButtonProps, 4 | } from "@nextui-org/react"; 5 | import { Icon } from "@iconify/react"; 6 | import { forwardRef } from "react"; 7 | 8 | export interface BaseButtonProps extends NextUIButtonProps { 9 | icon?: string; 10 | iconClassName?: string; 11 | label: string; 12 | loadingLabel?: string; 13 | loading?: boolean; 14 | iconPosition?: "start" | "end"; 15 | className?: string; 16 | color?: 17 | | "default" 18 | | "primary" 19 | | "secondary" 20 | | "success" 21 | | "warning" 22 | | "danger"; 23 | variant?: 24 | | "solid" 25 | | "bordered" 26 | | "light" 27 | | "flat" 28 | | "faded" 29 | | "shadow" 30 | | "ghost"; 31 | } 32 | 33 | export const BaseButton = forwardRef( 34 | ( 35 | { 36 | icon, 37 | iconClassName = "", 38 | label, 39 | loadingLabel, 40 | loading = false, 41 | iconPosition = "start", 42 | className = "", 43 | ...props 44 | }, 45 | ref 46 | ) => { 47 | const displayLabel = loading && loadingLabel ? loadingLabel : label; 48 | const iconContent = icon && ( 49 | 50 | ); 51 | 52 | return ( 53 | 65 | {displayLabel} 66 | 67 | ); 68 | } 69 | ); 70 | 71 | BaseButton.displayName = "BaseButton"; 72 | -------------------------------------------------------------------------------- /docs/layout-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # Layout Component 2 | 3 | ## Overview 4 | 5 | The Layout component serves as the main structural wrapper for pages and content sections in the application. It provides consistent spacing, padding, and structural organization across different views. 6 | 7 | ## Available Variants 8 | 9 | - Default Layout: Standard page layout with header and footer 10 | - Minimal Layout: Simplified version without header/footer for specific use cases 11 | - Full-width Layout: Extends to screen edges without side padding 12 | 13 | ## Props and Configuration 14 | 15 | | Prop | Type | Default | Description | 16 | | --------- | -------------------------------------- | --------- | ---------------------------------------- | 17 | | children | ReactNode | required | Content to be rendered within the layout | 18 | | variant | 'default' \| 'minimal' \| 'full-width' | 'default' | Layout style variant | 19 | | className | string | '' | Additional CSS classes | 20 | | padding | boolean | true | Enable/disable default padding | 21 | 22 | ## Usage Examples 23 | 24 | ```jsx 25 | // Basic usage 26 | 27 | 28 | 29 | 30 | // Minimal variant 31 | 32 | 33 | 34 | 35 | // Full-width with custom class 36 | 37 | 38 | 39 | ``` 40 | 41 | ## Testing Guidelines 42 | 43 | 1. Verify proper rendering of all layout variants 44 | 2. Test responsive behavior across different screen sizes 45 | 3. Validate proper spacing and padding application 46 | 4. Check accessibility compliance 47 | 5. Ensure proper propagation of className prop 48 | -------------------------------------------------------------------------------- /src/components/Card/StatusCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | 4 | export type StatusType = "success" | "warning" | "danger" | "default"; 5 | 6 | export interface StatusCardProps { 7 | status?: StatusType; 8 | title: string; 9 | description?: string; 10 | icon?: string; 11 | iconClassName?: string; 12 | className?: string; 13 | } 14 | 15 | const statusConfig: Record = 16 | { 17 | success: { bgColor: "bg-success-50", textColor: "text-success" }, 18 | warning: { bgColor: "bg-warning-50", textColor: "text-warning" }, 19 | danger: { bgColor: "bg-danger-50", textColor: "text-danger" }, 20 | default: { bgColor: "bg-default-50", textColor: "text-default-600" }, 21 | }; 22 | 23 | /** 24 | * StatusCard displays status information with consistent styling 25 | * @component 26 | * @example 27 | * ```tsx 28 | * 34 | * ``` 35 | */ 36 | export function StatusCard({ 37 | status = "default", 38 | title, 39 | description, 40 | icon, 41 | iconClassName, 42 | className = "", 43 | }: StatusCardProps) { 44 | const { bgColor, textColor } = statusConfig[status]; 45 | 46 | return ( 47 | 48 |
49 | {icon && ( 50 | 54 | )} 55 |
56 |

{title}

57 | {description &&

{description}

} 58 |
59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Modal/hooks/useSchemaForm.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from "react"; 2 | import { JSONSchema7 } from "json-schema"; 3 | import { 4 | getInitialValues, 5 | getValueAtPath, 6 | setValueAtPath, 7 | } from "../utils/form-state"; 8 | import { ValidationError, validateSchema } from "../utils/schema-utils"; 9 | 10 | export interface UseSchemaFormProps { 11 | schema: JSONSchema7; 12 | initialValues?: Record; 13 | } 14 | 15 | export interface UseSchemaFormResult { 16 | values: Record; 17 | errors: ValidationError[]; 18 | getFieldValue: (path: string[]) => unknown; 19 | setFieldValue: (path: string[], value: unknown) => void; 20 | setValues: (values: Record) => void; 21 | isValid: boolean; 22 | } 23 | 24 | export function useSchemaForm({ 25 | schema, 26 | initialValues, 27 | }: UseSchemaFormProps): UseSchemaFormResult { 28 | // Initialize form state with either provided values or defaults from schema 29 | const [values, setValues] = useState>(() => ({ 30 | ...getInitialValues(schema), 31 | ...initialValues, 32 | })); 33 | 34 | // Memoize validation errors 35 | const errors = useMemo( 36 | () => validateSchema(schema, values), 37 | [schema, values] 38 | ); 39 | 40 | // Memoize field value getter 41 | const getFieldValue = useCallback( 42 | (path: string[]) => getValueAtPath(values, path), 43 | [values] 44 | ); 45 | 46 | // Memoize field value setter 47 | const setFieldValue = useCallback((path: string[], value: unknown) => { 48 | setValues((prev) => setValueAtPath(prev, path, value)); 49 | }, []); 50 | 51 | // Memoize form validity 52 | const isValid = useMemo(() => errors.length === 0, [errors]); 53 | 54 | return { 55 | values, 56 | errors, 57 | getFieldValue, 58 | setFieldValue, 59 | setValues, 60 | isValid, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /docs/modal-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # Modal Component 2 | 3 | ## Overview 4 | 5 | The Modal component provides a reusable overlay dialog that can be used to display content on top of the main application. It handles accessibility, keyboard interactions, and backdrop clicks. 6 | 7 | ## Available Variants 8 | 9 | - Standard Modal 10 | - Full Screen Modal 11 | - Side Panel Modal 12 | 13 | ## Props and Configuration 14 | 15 | | Prop | Type | Default | Description | 16 | | -------- | ------------------------------ | -------- | ------------------------------ | 17 | | isOpen | boolean | false | Controls modal visibility | 18 | | onClose | function | required | Callback when modal closes | 19 | | children | ReactNode | required | Content to render inside modal | 20 | | title | string | '' | Modal header title | 21 | | size | 'sm' \| 'md' \| 'lg' \| 'full' | 'md' | Controls modal size | 22 | | position | 'center' \| 'right' | 'center' | Modal position on screen | 23 | 24 | ## Usage Examples 25 | 26 | ```jsx 27 | // Basic usage 28 | 29 | Title 30 | Content goes here 31 | 32 | 33 | 34 | 35 | 36 | // Full screen modal 37 | 38 | {/* Modal content */} 39 | 40 | ``` 41 | 42 | ## Testing Guidelines 43 | 44 | 1. Test modal open/close functionality 45 | 2. Verify backdrop click behavior 46 | 3. Test keyboard interactions (Esc key) 47 | 4. Check accessibility features: 48 | - Focus trap inside modal 49 | - ARIA attributes 50 | - Screen reader compatibility 51 | 5. Test different sizes and positions 52 | -------------------------------------------------------------------------------- /src/providers/gemini/utils/validation.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv"; 2 | 3 | const ajv = new Ajv({ allErrors: true }); 4 | 5 | export interface ValidationResult { 6 | data?: unknown; 7 | error?: string; 8 | } 9 | 10 | /** 11 | * Extracts JSON from a text response, handling various formats 12 | */ 13 | function extractJson(text: string): string { 14 | // Try to find JSON content between markers 15 | const jsonMatch = text.match(/\{[\s\S]*\}/); 16 | if (!jsonMatch) { 17 | throw new Error("No JSON object found in response"); 18 | } 19 | 20 | // Return the first complete JSON object found 21 | return jsonMatch[0]; 22 | } 23 | 24 | /** 25 | * Validates JSON data against a schema using AJV 26 | */ 27 | export function validateAgainstSchema( 28 | data: unknown, 29 | schema: Record 30 | ): ValidationResult { 31 | const validate = ajv.compile(schema); 32 | 33 | if (validate(data)) { 34 | return { data }; 35 | } 36 | 37 | const errors = validate.errors 38 | ?.map((error) => { 39 | const path = error.dataPath || error.schemaPath || ""; 40 | const msg = error.message || "Invalid"; 41 | return `${path} ${msg}`; 42 | }) 43 | .join("; "); 44 | 45 | return { error: `JSON validation failed: ${errors}` }; 46 | } 47 | 48 | /** 49 | * Attempts to parse and validate JSON response against a schema 50 | */ 51 | export function parseAndValidateJson( 52 | text: string, 53 | schema: Record 54 | ): ValidationResult { 55 | try { 56 | // First try to extract JSON content from the response 57 | const jsonContent = extractJson(text); 58 | 59 | // Then parse the extracted JSON 60 | const parsed = JSON.parse(jsonContent); 61 | return validateAgainstSchema(parsed, schema); 62 | } catch (error) { 63 | return { 64 | error: `Failed to parse JSON: ${ 65 | error instanceof Error ? error.message : "Unknown error" 66 | }`, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/google-auth/setup-google-env.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | // Get file paths from command line arguments or use defaults 6 | const credentialsPath = 7 | process.argv[2] || "credentials/google-credentials.json"; 8 | const tokenPath = process.argv[3] || "credentials/google-token.json"; 9 | 10 | try { 11 | // Read and encode the credentials file 12 | const credentials = fs.readFileSync(credentialsPath, "utf8"); 13 | const credentialsBase64 = Buffer.from(credentials).toString("base64"); 14 | 15 | // Read and encode the token file 16 | const token = fs.readFileSync(tokenPath, "utf8"); 17 | const tokenBase64 = Buffer.from(token).toString("base64"); 18 | 19 | // Prepare the .env content 20 | let envContent = ""; 21 | 22 | // Read existing .env if it exists 23 | if (fs.existsSync(".env")) { 24 | envContent = fs.readFileSync(".env", "utf8"); 25 | // Remove any existing Google credentials 26 | envContent = envContent 27 | .split("\n") 28 | .filter( 29 | (line) => 30 | !line.startsWith("VITE_GOOGLE_CREDENTIALS=") && 31 | !line.startsWith("VITE_GOOGLE_TOKEN=") 32 | ) 33 | .join("\n"); 34 | if (envContent && !envContent.endsWith("\n")) { 35 | envContent += "\n"; 36 | } 37 | } 38 | 39 | // Add the new credentials 40 | envContent += `VITE_GOOGLE_CREDENTIALS=${credentialsBase64}\n`; 41 | envContent += `VITE_GOOGLE_TOKEN=${tokenBase64}\n`; 42 | 43 | // Write to .env file 44 | fs.writeFileSync(".env", envContent); 45 | 46 | console.log("Successfully wrote Google credentials and token to .env"); 47 | console.log(`Credentials file used: ${path.resolve(credentialsPath)}`); 48 | console.log(`Token file used: ${path.resolve(tokenPath)}`); 49 | } catch (error) { 50 | console.error("Error:", error.message); 51 | console.log( 52 | "Default paths: credentials/google-credentials.json and credentials/google-token.json" 53 | ); 54 | process.exit(1); 55 | } 56 | -------------------------------------------------------------------------------- /src/types/systemprompt.d.ts: -------------------------------------------------------------------------------- 1 | export interface SystempromptBlock { 2 | id: string; 3 | type: string; 4 | content: string; 5 | metadata: Metadata; 6 | prefix: string; 7 | _link: string; 8 | } 9 | 10 | export interface SystempromptAgent { 11 | id: string; 12 | type: string; 13 | content: string; 14 | metadata: Metadata; 15 | _link: string; 16 | } 17 | 18 | type Metadata = { 19 | title: string; 20 | description: string; 21 | created: string; 22 | updated: string; 23 | version: number; 24 | status: string; 25 | tag: string[]; 26 | }; 27 | 28 | export interface SystempromptPrompt { 29 | id?: string; 30 | instruction?: { 31 | static: string; 32 | dynamic: string; 33 | state: string; 34 | }; 35 | input?: { 36 | name: string; 37 | description: string; 38 | schema: { 39 | type: string; 40 | required?: string[]; 41 | properties?: Record; 42 | description?: string; 43 | additionalProperties?: boolean; 44 | }; 45 | type: string[]; 46 | reference: unknown[]; 47 | }; 48 | output?: { 49 | name: string; 50 | description: string; 51 | schema: { 52 | type: string; 53 | required?: string[]; 54 | properties?: Record; 55 | description?: string; 56 | additionalProperties?: boolean; 57 | }; 58 | }; 59 | metadata: Metadata; 60 | _meta?: unknown[]; 61 | } 62 | 63 | export interface SystempromptModule { 64 | id: string; 65 | type: string; 66 | title: string; 67 | description: string; 68 | environment_variables: string[]; 69 | github_link: string; 70 | npm_link: string; 71 | icon: string; 72 | metadata: ServerMetadata; 73 | block: SystempromptBlock[]; 74 | prompt: SystempromptPrompt[]; 75 | agent: SystempromptAgent[]; 76 | _link: string; 77 | } 78 | 79 | export interface SystempromptUser { 80 | user: { 81 | name: string; 82 | email: string; 83 | roles: string[]; 84 | }; 85 | billing: null; 86 | api_key: string; 87 | } 88 | -------------------------------------------------------------------------------- /proxy/src/types/systemprompt.d.ts: -------------------------------------------------------------------------------- 1 | export interface SystempromptBlock { 2 | id: string; 3 | type: string; 4 | content: string; 5 | metadata: Metadata; 6 | prefix: string; 7 | _link: string; 8 | } 9 | 10 | export interface SystempromptAgent { 11 | id: string; 12 | type: string; 13 | content: string; 14 | metadata: Metadata; 15 | _link: string; 16 | } 17 | 18 | type Metadata = { 19 | title: string; 20 | description: string; 21 | created: string; 22 | updated: string; 23 | version: number; 24 | status: string; 25 | tag: string[]; 26 | }; 27 | 28 | export interface SystempromptPrompt { 29 | id?: string; 30 | instruction?: { 31 | static: string; 32 | dynamic: string; 33 | state: string; 34 | }; 35 | input?: { 36 | name: string; 37 | description: string; 38 | schema: { 39 | type: string; 40 | required?: string[]; 41 | properties?: Record; 42 | description?: string; 43 | additionalProperties?: boolean; 44 | }; 45 | type: string[]; 46 | reference: unknown[]; 47 | }; 48 | output?: { 49 | name: string; 50 | description: string; 51 | schema: { 52 | type: string; 53 | required?: string[]; 54 | properties?: Record; 55 | description?: string; 56 | additionalProperties?: boolean; 57 | }; 58 | }; 59 | metadata: Metadata; 60 | _meta?: unknown[]; 61 | } 62 | 63 | export interface SystempromptModule { 64 | id: string; 65 | type: string; 66 | title: string; 67 | description: string; 68 | environment_variables: string[]; 69 | github_link: string; 70 | npm_link: string; 71 | icon: string; 72 | metadata: ServerMetadata; 73 | block: SystempromptBlock[]; 74 | prompt: SystempromptPrompt[]; 75 | agent: SystempromptAgent[]; 76 | _link: string; 77 | } 78 | 79 | export interface SystempromptUser { 80 | user: { 81 | name: string; 82 | email: string; 83 | roles: string[]; 84 | }; 85 | billing: null; 86 | api_key: string; 87 | } 88 | -------------------------------------------------------------------------------- /proxy/dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { config } from "dotenv"; 3 | import { EventSource } from "eventsource"; 4 | import { parseArgs } from "node:util"; 5 | import chalk from "chalk"; 6 | import { ProxyServer } from "./server.js"; 7 | // Load environment variables from .env file 8 | config(); 9 | globalThis.EventSource = EventSource; 10 | // Parse command line arguments 11 | const { values } = parseArgs({ 12 | args: process.argv.slice(2), 13 | options: { 14 | port: { type: "string", default: "3000" }, 15 | }, 16 | }); 17 | // Print banner 18 | function printBanner() { 19 | const version = "v0.3.13"; // TODO: Get this from package.json 20 | console.log(chalk.cyan("\n┌" + "─".repeat(60) + "┐")); 21 | console.log(chalk.cyan("│") + 22 | " ".repeat(15) + 23 | chalk.bold("Systemprompt MCP Server") + 24 | " ".repeat(15) + 25 | chalk.dim(version) + 26 | " ".repeat(3) + 27 | chalk.cyan("│")); 28 | console.log(chalk.cyan("└" + "─".repeat(60) + "┘\n")); 29 | } 30 | // Start server 31 | export async function main() { 32 | printBanner(); 33 | try { 34 | const server = await ProxyServer.create(); 35 | await server.startServer(parseInt(values.port)); 36 | } 37 | catch (error) { 38 | console.error("\n" + 39 | chalk.red("╔═ Error ═══════════════════════════════════════════════════════════╗")); 40 | console.error(chalk.red("║ ") + 41 | chalk.yellow("Failed to start server:") + 42 | " ".repeat(39) + 43 | chalk.red("║")); 44 | console.error(chalk.red("║ ") + 45 | chalk.white(error.message) + 46 | " ".repeat(Math.max(0, 57 - error.message.length)) + 47 | chalk.red("║")); 48 | console.error(chalk.red("╚════════════════════════════════════════════════════════════════════╝\n")); 49 | process.exit(1); 50 | } 51 | } 52 | const isMainModule = process.argv[1] && 53 | import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/")); 54 | if (isMainModule) { 55 | main(); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Card/AccordionCard.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Accordion, AccordionItem, Card, CardBody } from "@nextui-org/react"; 3 | 4 | interface AccordionCardProps { 5 | /** Content to show in the accordion header */ 6 | header: ReactNode; 7 | /** Content to show in the accordion header when collapsed (optional) */ 8 | collapsedContent?: ReactNode; 9 | /** Whether the accordion is expanded by default */ 10 | defaultExpanded?: boolean; 11 | /** The main content of the card */ 12 | children: ReactNode; 13 | /** Additional class names */ 14 | className?: string; 15 | } 16 | 17 | /** 18 | * A card component that combines Card with an accordion for collapsible content. 19 | * The header is part of the accordion itself for a more integrated look. 20 | */ 21 | export function AccordionCard({ 22 | header, 23 | collapsedContent, 24 | defaultExpanded = false, 25 | children, 26 | className = "", 27 | }: AccordionCardProps) { 28 | return ( 29 | 30 | 31 | 36 | 47 | {header} 48 | {collapsedContent && ( 49 |
50 | {collapsedContent} 51 |
52 | )} 53 |
54 | } 55 | > 56 | {children} 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Modal/schema-utils.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from "openapi-client-axios"; 2 | 3 | export interface ValidationError { 4 | path: string[]; 5 | message: string; 6 | } 7 | 8 | export function validateSchema( 9 | schema: OpenAPIV3.SchemaObject, 10 | data: unknown, 11 | path: string[] = [] 12 | ): ValidationError[] { 13 | const errors: ValidationError[] = []; 14 | 15 | if (schema.required && Array.isArray(schema.required)) { 16 | for (const requiredField of schema.required) { 17 | if ( 18 | data === undefined || 19 | data === null || 20 | !isRecord(data) || 21 | !(requiredField in data) 22 | ) { 23 | errors.push({ 24 | path: [...path, requiredField], 25 | message: "This field is required", 26 | }); 27 | } 28 | } 29 | } 30 | 31 | if (schema.oneOf) { 32 | const validSchemas = schema.oneOf.filter((subSchema) => { 33 | const subErrors = validateSchema( 34 | subSchema as OpenAPIV3.SchemaObject, 35 | data, 36 | path 37 | ); 38 | return subErrors.length === 0; 39 | }); 40 | 41 | if (validSchemas.length === 0) { 42 | errors.push({ 43 | path, 44 | message: "Data does not match any of the allowed schemas", 45 | }); 46 | } 47 | } 48 | 49 | if (schema.properties && isRecord(data)) { 50 | for (const [key, propSchema] of Object.entries(schema.properties)) { 51 | if (key in data) { 52 | const propErrors = validateSchema( 53 | propSchema as OpenAPIV3.SchemaObject, 54 | data[key], 55 | [...path, key] 56 | ); 57 | errors.push(...propErrors); 58 | } 59 | } 60 | } 61 | 62 | if (schema.enum) { 63 | if (data !== undefined && !schema.enum.includes(data)) { 64 | errors.push({ 65 | path, 66 | message: `Must be one of: ${schema.enum.join(", ")}`, 67 | }); 68 | } 69 | } 70 | 71 | return errors; 72 | } 73 | 74 | function isRecord(value: unknown): value is Record { 75 | return typeof value === "object" && value !== null; 76 | } 77 | -------------------------------------------------------------------------------- /src/components/StatusIndicator/StatusIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@iconify/react"; 2 | 3 | export interface StatusIndicatorProps { 4 | type?: "success" | "warning" | "danger" | "default"; 5 | title: string; 6 | description?: string; 7 | icon?: string; 8 | className?: string; 9 | } 10 | 11 | export function StatusIndicator({ 12 | type = "default", 13 | title, 14 | description = "", 15 | icon, 16 | className = "", 17 | }: StatusIndicatorProps) { 18 | const getTypeStyles = () => { 19 | switch (type) { 20 | case "success": 21 | return { 22 | icon: icon || "solar:shield-check-line-duotone", 23 | iconClass: "text-success", 24 | bgClass: "bg-success-50/50", 25 | }; 26 | case "warning": 27 | return { 28 | icon: icon || "solar:shield-warning-line-duotone", 29 | iconClass: "text-warning", 30 | bgClass: "bg-warning-50/50", 31 | }; 32 | case "danger": 33 | return { 34 | icon: icon || "solar:shield-warning-line-duotone", 35 | iconClass: "text-danger", 36 | bgClass: "bg-danger-50/50", 37 | }; 38 | default: 39 | return { 40 | icon: icon || "solar:shield-minimalistic-line-duotone", 41 | iconClass: "text-default-400", 42 | bgClass: "bg-default-50", 43 | }; 44 | } 45 | }; 46 | 47 | const styles = getTypeStyles(); 48 | 49 | return ( 50 |
54 | 61 |
62 |

{title}

63 | {description && ( 64 |

{description}

65 | )} 66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/StatusIndicator/__tests__/StatusIndicator.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import "@testing-library/jest-dom"; 4 | import { StatusIndicator } from "../StatusIndicator"; 5 | 6 | // Mock the Icon component 7 | vi.mock("@iconify/react", () => ({ 8 | Icon: ({ 9 | icon, 10 | className, 11 | "data-testid": testId, 12 | }: { 13 | icon: string; 14 | className?: string; 15 | "data-testid"?: string; 16 | }) => ( 17 |
18 | {icon} 19 |
20 | ), 21 | })); 22 | 23 | describe("StatusIndicator", () => { 24 | it("renders with default props", () => { 25 | render(); 26 | 27 | expect(screen.getByText("Test Status")).toBeInTheDocument(); 28 | const icon = screen.getByTestId("status-icon-default"); 29 | expect(icon).toBeInTheDocument(); 30 | expect(icon).toHaveAttribute( 31 | "data-icon", 32 | "solar:shield-minimalistic-line-duotone" 33 | ); 34 | }); 35 | 36 | it("renders with description", () => { 37 | render( 38 | 39 | ); 40 | 41 | expect(screen.getByText("Test Status")).toBeInTheDocument(); 42 | expect(screen.getByText("Test Description")).toBeInTheDocument(); 43 | }); 44 | 45 | it("renders with custom type", () => { 46 | render(); 47 | 48 | const container = screen.getByTestId("status-container-success"); 49 | expect(container).toHaveClass("bg-success-50/50"); 50 | 51 | const icon = screen.getByTestId("status-icon-success"); 52 | expect(icon).toHaveAttribute( 53 | "data-icon", 54 | "solar:shield-check-line-duotone" 55 | ); 56 | }); 57 | 58 | it("renders with custom icon", () => { 59 | render(); 60 | 61 | const icon = screen.getByTestId("status-icon-default"); 62 | expect(icon).toHaveAttribute("data-icon", "custom-icon"); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/Card/__tests__/ServerCard.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { render, screen } from "@testing-library/react"; 3 | import "@testing-library/jest-dom"; 4 | import { ServerCard } from "../ServerCard"; 5 | 6 | // Mock the Icon component 7 | vi.mock("@iconify/react", () => ({ 8 | Icon: ({ 9 | icon, 10 | className, 11 | "data-testid": testId, 12 | }: { 13 | icon: string; 14 | className?: string; 15 | "data-testid"?: string; 16 | }) => ( 17 |
18 | {icon} 19 |
20 | ), 21 | })); 22 | 23 | describe("ServerCard", () => { 24 | const defaultProps = { 25 | serverName: "Test Server", 26 | serverId: "test-123", 27 | isConnected: true, 28 | }; 29 | 30 | it("renders server name and ID", () => { 31 | render(); 32 | expect(screen.getByText("Test Server")).toBeInTheDocument(); 33 | expect(screen.getByText("test-123")).toBeInTheDocument(); 34 | }); 35 | 36 | it("shows connected status with primary color", () => { 37 | render(); 38 | const icon = screen.getByTestId("server-icon-connected"); 39 | const iconContainer = icon.closest("div.p-2"); 40 | expect(icon).toBeInTheDocument(); 41 | expect(iconContainer).toHaveClass("text-primary"); 42 | expect(icon).toHaveAttribute("data-icon", "solar:server-line-duotone"); 43 | }); 44 | 45 | it("shows disconnected status with default color", () => { 46 | render(); 47 | const icon = screen.getByTestId("server-icon-disconnected"); 48 | const iconContainer = icon.closest("div.p-2"); 49 | expect(icon).toBeInTheDocument(); 50 | expect(iconContainer).toHaveClass("text-default-400"); 51 | expect(icon).toHaveAttribute("data-icon", "solar:server-line-duotone"); 52 | }); 53 | 54 | it("renders children content", () => { 55 | render( 56 | 57 |
Test Content
58 |
59 | ); 60 | expect(screen.getByText("Test Content")).toBeInTheDocument(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /proxy/src/mcpProxy.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; 2 | import { McpHandlers } from "./handlers/mcpHandlers.js"; 3 | import { ConfigHandlers } from "./handlers/configHandlers.js"; 4 | import { TransportHandlers } from "./handlers/transportHandlers.js"; 5 | import { defaults } from "./config/defaults.js"; 6 | import type { McpConfig } from "./types/index.js"; 7 | 8 | export default function mcpProxy({ 9 | transportToClient, 10 | transportToServer, 11 | onerror, 12 | }: { 13 | transportToClient: Transport; 14 | transportToServer: Transport; 15 | onerror: (error: Error) => void; 16 | }) { 17 | let transportToClientClosed = false; 18 | let transportToServerClosed = false; 19 | 20 | transportToClient.onmessage = (message) => { 21 | transportToServer.send(message).catch(onerror); 22 | }; 23 | 24 | transportToServer.onmessage = (message) => { 25 | transportToClient.send(message).catch(onerror); 26 | }; 27 | 28 | transportToClient.onclose = () => { 29 | if (transportToServerClosed) { 30 | return; 31 | } 32 | 33 | transportToClientClosed = true; 34 | transportToServer.close().catch(onerror); 35 | }; 36 | 37 | transportToServer.onclose = () => { 38 | if (transportToClientClosed) { 39 | return; 40 | } 41 | transportToServerClosed = true; 42 | 43 | transportToClient.close().catch(onerror); 44 | }; 45 | 46 | transportToClient.onerror = onerror; 47 | transportToServer.onerror = onerror; 48 | } 49 | 50 | export class McpProxy { 51 | private mcpHandlers: McpHandlers; 52 | private configHandlers: ConfigHandlers; 53 | private transportHandlers: TransportHandlers; 54 | 55 | constructor(config: McpConfig) { 56 | // Ensure all required properties are initialized 57 | const initializedConfig: McpConfig = { 58 | mcpServers: config.mcpServers || {}, 59 | available: config.available || {}, 60 | defaults: config.defaults || defaults, 61 | }; 62 | 63 | this.mcpHandlers = new McpHandlers(initializedConfig); 64 | this.configHandlers = new ConfigHandlers(initializedConfig); 65 | this.transportHandlers = new TransportHandlers(initializedConfig); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/google-auth/README.md: -------------------------------------------------------------------------------- 1 | # Google Authentication Setup 2 | 3 | This script handles Google OAuth2 authentication for the MCP server. It manages credentials, tokens, and environment variables needed for Google API access. 4 | 5 | ## Prerequisites 6 | 7 | 1. **Google Cloud Console Setup**: 8 | 9 | - Create a project in Google Cloud Console 10 | - Enable the APIs you need (Gmail, Calendar, Drive) 11 | - Create OAuth 2.0 credentials for a desktop application 12 | - Set the redirect URI to: `http://localhost:3333/oauth2callback` 13 | 14 | 2. **Credentials File**: 15 | - Download your OAuth 2.0 credentials from Google Cloud Console 16 | - Save them as `scripts/google-auth/credentials/google-credentials.json` 17 | - The file should have this structure: 18 | ```json 19 | { 20 | "installed": { 21 | "client_id": "your-client-id", 22 | "client_secret": "your-client-secret", 23 | "redirect_uris": ["http://localhost:3333/oauth2callback"] 24 | } 25 | } 26 | ``` 27 | 28 | ## Running the Script 29 | 30 | From the project root, run: 31 | 32 | ```bash 33 | npm run setup:google 34 | ``` 35 | 36 | The script will: 37 | 38 | 1. Create necessary directories if they don't exist 39 | 2. Install required dependencies 40 | 3. Open your browser for Google authentication 41 | 4. Save the authentication token to `scripts/google-auth/credentials/google-token.json` 42 | 5. Save base64-encoded credentials and token to the root `.env` file 43 | 44 | ## Environment Variables 45 | 46 | The script will add/update these variables in your root `.env`: 47 | 48 | - `VITE_GOOGLE_CREDENTIALS`: Base64 encoded OAuth2 credentials 49 | - `VITE_GOOGLE_TOKEN`: Base64 encoded authentication token 50 | 51 | ## Troubleshooting 52 | 53 | 1. **Port 3333 in Use**: 54 | 55 | - Ensure no other service is using port 3333 56 | - The script needs this port for the OAuth callback 57 | 58 | 2. **Browser Doesn't Open**: 59 | 60 | - The script will display a URL 61 | - Copy and paste it into your browser manually 62 | 63 | 3. **Authentication Fails**: 64 | - Verify your credentials are correct 65 | - Ensure the redirect URI matches exactly 66 | - Check that required APIs are enabled in Google Cloud Console 67 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/utils/README.md: -------------------------------------------------------------------------------- 1 | # Multimodal Agent Utilities 2 | 3 | This directory contains utility functions for the multimodal agent implementation, particularly focusing on mapping between JSON Schema and Gemini's type system. 4 | 5 | ## Tool Mappers 6 | 7 | The `tool-mappers.ts` file provides functionality to convert JSON Schema definitions to Gemini-compatible format, with special handling for various schema types and protected keywords. 8 | 9 | ### Protected Keywords 10 | 11 | When mapping property names, certain keywords are protected and automatically prefixed with `safe_` to prevent conflicts: 12 | 13 | - `from` → `safe_from` 14 | 15 | This transformation is applied recursively to all object properties, including nested objects and array items. 16 | 17 | ### Key Functions 18 | 19 | - `mapPropertyType`: Converts JSON Schema types to Gemini schema types 20 | - `mapToolProperties`: Maps MCP tool definitions to Gemini-compatible format 21 | - `safePropertyName`: Handles protected keyword transformation 22 | - `sanitizeFunctionName`: Ensures function names are valid identifiers 23 | 24 | ### Example Usage 25 | 26 | ```typescript 27 | import { mapToolsToGeminiFormat } from "./tool-mappers"; 28 | 29 | const tools = [ 30 | { 31 | name: "auth", 32 | description: "Authentication tool", 33 | inputSchema: { 34 | type: "object", 35 | properties: { 36 | token: { type: "string" }, // Will be mapped to safe_token 37 | from: { type: "string" }, // Will be mapped to safe_from 38 | }, 39 | }, 40 | }, 41 | ]; 42 | 43 | const geminiTools = mapToolsToGeminiFormat(tools); 44 | ``` 45 | 46 | ### Type Mappings 47 | 48 | | JSON Schema Type | Gemini Type | 49 | | ---------------- | ----------- | 50 | | string | STRING | 51 | | number/integer | NUMBER | 52 | | boolean | BOOLEAN | 53 | | array | ARRAY | 54 | | object | OBJECT | 55 | | null | STRING | 56 | 57 | ### Special Cases 58 | 59 | 1. Objects with no properties get a default `_data` field 60 | 2. Arrays with no item type default to STRING items 61 | 3. Protected keywords are automatically prefixed with `safe_` 62 | 4. Required fields are preserved in the mapping 63 | -------------------------------------------------------------------------------- /config/server.config.ts: -------------------------------------------------------------------------------- 1 | import { SidebarItem } from "@/components/sidebar/types"; 2 | import { McpData, ServerMetadata } from "@/types/server.types"; 3 | 4 | /** 5 | * Gets the configuration for a single server 6 | */ 7 | export function getServerConfig( 8 | id: string, 9 | mcpData: McpData, 10 | serverMetadata?: ServerMetadata, 11 | isConnected: boolean = false 12 | ): SidebarItem { 13 | if (!mcpData) throw new Error("MCP data not available"); 14 | 15 | const serverConfig = mcpData.mcpServers[id]; 16 | if (!serverConfig) 17 | throw new Error(`No configuration found for server4: ${id}`); 18 | 19 | const metadata = { 20 | ...serverConfig.metadata, 21 | ...serverMetadata, 22 | color: isConnected 23 | ? serverMetadata?.color || serverConfig.metadata?.color || "secondary" 24 | : "secondary", 25 | }; 26 | 27 | const name = 28 | metadata.name || `${id.charAt(0).toUpperCase()}${id.slice(1)} Server`; 29 | 30 | return { 31 | key: `server-${id}`, 32 | label: name, 33 | icon: metadata.icon, 34 | color: metadata.color as 35 | | "secondary" 36 | | "success" 37 | | "warning" 38 | | "primary" 39 | | undefined, 40 | href: `/servers/${id}`, 41 | description: metadata.description || name, 42 | serverId: id, 43 | metadata, 44 | }; 45 | } 46 | 47 | /** 48 | * Gets configurations for all servers 49 | */ 50 | export function getServerConfigs(mcpData: McpData): SidebarItem[] { 51 | if (!mcpData) throw new Error("MCP data not available"); 52 | return Object.keys(mcpData.mcpServers).map((id) => 53 | getServerConfig(id, mcpData) 54 | ); 55 | } 56 | 57 | /** 58 | * Gets a mapping of server IDs to their display names 59 | */ 60 | export function getServerNames(mcpData: McpData): Record { 61 | if (!mcpData) throw new Error("MCP data not available"); 62 | return Object.keys(mcpData.mcpServers).reduce((names, id) => { 63 | names[id] = getServerConfig(id, mcpData).label; 64 | return names; 65 | }, {} as Record); 66 | } 67 | 68 | export function formatServerLabel( 69 | id: string, 70 | metadata?: ServerMetadata 71 | ): string { 72 | return metadata?.name || `${id.charAt(0).toUpperCase()}${id.slice(1)} Server`; 73 | } 74 | -------------------------------------------------------------------------------- /proxy/dist/server.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from "vitest"; 2 | import supertest from "supertest"; 3 | import { ProxyServer } from "./server.js"; 4 | describe("ProxyServer", () => { 5 | let server; 6 | let request = {}; 7 | const mockConfig = { 8 | sse: { 9 | systemprompt: { 10 | url: "http://localhost:3001", 11 | apiKey: "test-key", 12 | }, 13 | }, 14 | mcpServers: { 15 | test: { 16 | command: "echo", 17 | args: ["test"], 18 | }, 19 | }, 20 | }; 21 | beforeEach(() => { 22 | server = new ProxyServer(mockConfig); 23 | request = supertest(server.getExpressApp()); 24 | }); 25 | describe("GET /config", () => { 26 | it("should return server configuration", async () => { 27 | const response = await request.get("/config"); 28 | expect(response.status).toBe(200); 29 | expect(response.body).toEqual({ 30 | mcpServers: mockConfig.mcpServers, 31 | }); 32 | }); 33 | }); 34 | describe("GET /sse", () => { 35 | it("should return 400 when serverId is missing", async () => { 36 | const response = await request.get("/sse"); 37 | expect(response.status).toBe(500); 38 | expect(response.body.error).toBe("Server ID must be specified"); 39 | }); 40 | it("should return 400 for invalid transport type", async () => { 41 | const response = await request 42 | .get("/sse") 43 | .query({ serverId: "test", transportType: "invalid" }); 44 | expect(response.status).toBe(500); 45 | expect(response.body.error).toBe("Invalid transport type specified"); 46 | }); 47 | }); 48 | describe("POST /message", () => { 49 | it("should return 404 when session is not found", async () => { 50 | const response = await request 51 | .post("/message") 52 | .query({ sessionId: "non-existent" }); 53 | expect(response.status).toBe(404); 54 | expect(response.text).toBe("Session not found"); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/shared/Input/ApiKeyInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState, ReactNode } from "react"; 2 | import { Input } from "@nextui-org/react"; 3 | import { EyeIcon, EyeOffIcon } from "lucide-react"; 4 | 5 | interface ApiKeyInputProps { 6 | value: string; 7 | onChange: (value: string) => void; 8 | label: string; 9 | description: ReactNode; 10 | placeholder?: string; 11 | isRequired?: boolean; 12 | } 13 | 14 | export function ApiKeyInput({ 15 | value, 16 | onChange, 17 | label, 18 | description, 19 | placeholder = "Enter API key", 20 | isRequired = false, 21 | }: ApiKeyInputProps) { 22 | const [isVisible, setIsVisible] = useState(false); 23 | const toggleVisibility = () => setIsVisible(!isVisible); 24 | 25 | return ( 26 | onChange(e.target.value)} 30 | description={description} 31 | placeholder={placeholder} 32 | labelPlacement="outside" 33 | isRequired={isRequired} 34 | type={isVisible ? "text" : "password"} 35 | variant="flat" 36 | classNames={{ 37 | base: "max-w-full", 38 | label: "text-sm font-medium text-white", 39 | description: "text-xs text-gray-400", 40 | inputWrapper: [ 41 | "border-1", 42 | "hover:border-blue-500 dark:hover:border-blue-400", 43 | "focus-within:!border-blue-500 dark:focus-within:!border-blue-400", 44 | "transition-colors", 45 | "rounded-lg", 46 | "!cursor-text", 47 | "h-12", 48 | ], 49 | input: [ 50 | "text-base", 51 | "text-gray-900 dark:text-white", 52 | "placeholder:text-gray-500 dark:placeholder:text-gray-400", 53 | ], 54 | }} 55 | endContent={ 56 | 67 | } 68 | /> 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/features/llm-registry/__tests__/LlmRegistryContext.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { renderHook, act } from "@testing-library/react"; 3 | import { 4 | useLlmRegistry, 5 | LlmRegistryProvider, 6 | } from "../contexts/LlmRegistryContext"; 7 | 8 | describe("LlmRegistryContext", () => { 9 | it("should throw error when used outside provider", () => { 10 | const consoleError = vi 11 | .spyOn(console, "error") 12 | .mockImplementation(() => {}); 13 | expect(() => { 14 | renderHook(() => useLlmRegistry()); 15 | }).toThrow("useLlmRegistry must be used within a LlmRegistryProvider"); 16 | consoleError.mockRestore(); 17 | }); 18 | 19 | it("should provide registry context when used within provider", () => { 20 | const { result } = renderHook(() => useLlmRegistry(), { 21 | wrapper: LlmRegistryProvider, 22 | }); 23 | 24 | expect(result.current.providers).toEqual([]); 25 | expect(typeof result.current.registerProvider).toBe("function"); 26 | expect(typeof result.current.unregisterProvider).toBe("function"); 27 | expect(typeof result.current.getProviderInstance).toBe("function"); 28 | }); 29 | 30 | it("should register and unregister providers", () => { 31 | const { result } = renderHook(() => useLlmRegistry(), { 32 | wrapper: LlmRegistryProvider, 33 | }); 34 | 35 | const mockProvider = { 36 | id: "test-provider", 37 | name: "Test Provider", 38 | description: "A test provider", 39 | configSchema: {}, 40 | }; 41 | 42 | const mockInstance = { 43 | id: "test-provider", 44 | name: "Test Provider", 45 | executePrompt: vi.fn(), 46 | isLoading: false, 47 | error: null, 48 | }; 49 | 50 | act(() => { 51 | result.current.registerProvider(mockProvider, mockInstance); 52 | }); 53 | expect(result.current.providers).toContainEqual(mockProvider); 54 | expect(result.current.getProviderInstance("test-provider")).toBe( 55 | mockInstance 56 | ); 57 | 58 | act(() => { 59 | result.current.unregisterProvider("test-provider"); 60 | }); 61 | expect(result.current.providers).not.toContainEqual(mockProvider); 62 | expect(result.current.getProviderInstance("test-provider")).toBeNull(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/hooks/useMcpClient.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 3 | import type { McpClientState } from "../types/McpContext.types"; 4 | import type { ProgressNotification } from "@modelcontextprotocol/sdk/types.js"; 5 | 6 | export const DEFAULT_CLIENT_STATE: McpClientState = { 7 | client: null, 8 | connectionStatus: "disconnected", 9 | serverType: "stdio", 10 | serverUrl: "", 11 | apiKey: "", 12 | resources: [], 13 | prompts: [], 14 | tools: [], 15 | loadedResources: [], 16 | }; 17 | 18 | export function useMcpClient() { 19 | const [clients, setClients] = useState>({}); 20 | 21 | const updateClientState = useCallback( 22 | (serverId: string, update: Partial) => { 23 | setClients((prev) => { 24 | const currentState = prev[serverId] || DEFAULT_CLIENT_STATE; 25 | const newState = { 26 | ...prev, 27 | [serverId]: { 28 | ...currentState, 29 | ...update, 30 | }, 31 | }; 32 | return newState; 33 | }); 34 | }, 35 | [clients] 36 | ); 37 | 38 | const setupClientNotifications = useCallback( 39 | (serverId: string, client: Client) => { 40 | client.fallbackNotificationHandler = async (notification: { 41 | method: string; 42 | params?: unknown; 43 | }) => { 44 | if (notification.method === "notifications/progress") { 45 | const params = notification.params as ProgressNotification["params"]; 46 | const clientState = clients[serverId] || DEFAULT_CLIENT_STATE; 47 | if (clientState?.onProgress) { 48 | const progressPercent = params.total 49 | ? Math.round((params.progress / params.total) * 100) 50 | : params.progress; 51 | clientState.onProgress(`Progress: ${progressPercent}%`); 52 | } 53 | } 54 | }; 55 | }, 56 | [clients] 57 | ); 58 | 59 | return { 60 | clients, 61 | activeClients: Object.entries(clients) 62 | .filter(([, { connectionStatus }]) => connectionStatus === "connected") 63 | .map(([id]) => id), 64 | updateClientState, 65 | setupClientNotifications, 66 | DEFAULT_CLIENT_STATE, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/features/multimodal-agent/lib/utils.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export type GetAudioContextOptions = AudioContextOptions & { 4 | id?: string; 5 | }; 6 | 7 | const map: Map = new Map(); 8 | 9 | export const audioContext: ( 10 | options?: GetAudioContextOptions 11 | ) => Promise = (() => { 12 | const didInteract = 13 | typeof window !== "undefined" 14 | ? new Promise((res) => { 15 | window.addEventListener("pointerdown", res, { once: true }); 16 | window.addEventListener("keydown", res, { once: true }); 17 | }) 18 | : Promise.resolve(); // Ensure it resolves immediately if window is not defined 19 | 20 | return async (options?: GetAudioContextOptions) => { 21 | try { 22 | const a = new Audio(); 23 | a.src = 24 | "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"; 25 | await a.play(); 26 | if (options?.id && map.has(options.id)) { 27 | const ctx = map.get(options.id); 28 | if (ctx) { 29 | return ctx; 30 | } 31 | } 32 | const ctx = new AudioContext(options); 33 | if (options?.id) { 34 | map.set(options.id, ctx); 35 | } 36 | return ctx; 37 | } catch (e) { 38 | await didInteract; 39 | if (options?.id && map.has(options.id)) { 40 | const ctx = map.get(options.id); 41 | if (ctx) { 42 | return ctx; 43 | } 44 | } 45 | const ctx = new AudioContext(options); 46 | if (options?.id) { 47 | map.set(options.id, ctx); 48 | } 49 | return ctx; 50 | } 51 | }; 52 | })(); 53 | 54 | export const blobToJSON = (blob: Blob) => 55 | new Promise((resolve, reject) => { 56 | const reader = new FileReader(); 57 | reader.onload = () => { 58 | if (reader.result) { 59 | const json = JSON.parse(reader.result as string); 60 | resolve(json); 61 | } else { 62 | reject("oops"); 63 | } 64 | }; 65 | reader.readAsText(blob); 66 | }); 67 | 68 | export function base64ToArrayBuffer(base64: string) { 69 | const binaryString = atob(base64); 70 | const bytes = new Uint8Array(binaryString.length); 71 | for (let i = 0; i < binaryString.length; i++) { 72 | bytes[i] = binaryString.charCodeAt(i); 73 | } 74 | return bytes.buffer; 75 | } 76 | -------------------------------------------------------------------------------- /docs/chip-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # Chip Components 2 | 3 | ## Overview 4 | 5 | Chip components provide compact elements for displaying information, tags, or status indicators. 6 | 7 | ## Available Components 8 | 9 | ### BaseChip 10 | 11 | Foundation chip component with customizable appearance. 12 | 13 | ```tsx 14 | {}} icon="tag" /> 15 | ``` 16 | 17 | ### StatusChip 18 | 19 | Specialized chip for displaying status information. 20 | 21 | ```tsx 22 | 23 | ``` 24 | 25 | ### FilterChip 26 | 27 | Interactive chip for filter selections. 28 | 29 | ```tsx 30 | {}} /> 31 | ``` 32 | 33 | ## Props 34 | 35 | ### BaseChip Props 36 | 37 | - `label: string` - Chip text 38 | - `color?: ChipColor` - Color variant 39 | - `size?: 'sm' | 'md' | 'lg'` - Chip size 40 | - `icon?: string` - Optional icon 41 | - `onClose?: () => void` - Optional close handler 42 | - `className?: string` - Additional CSS classes 43 | 44 | ### StatusChip Props 45 | 46 | - `status: 'success' | 'warning' | 'error' | 'info'` - Status type 47 | - `label: string` - Status text 48 | - `icon?: string` - Optional status icon 49 | - `className?: string` - Additional CSS classes 50 | 51 | ### FilterChip Props 52 | 53 | - `label: string` - Filter text 54 | - `selected: boolean` - Selection state 55 | - `onChange: (selected: boolean) => void` - Selection handler 56 | - `disabled?: boolean` - Disabled state 57 | - `className?: string` - Additional CSS classes 58 | 59 | ## Testing Guidelines 60 | 61 | 1. Test chip states: 62 | 63 | - Default render 64 | - With/without icon 65 | - With/without close button 66 | - Different colors and sizes 67 | 68 | 2. Test interactions: 69 | 70 | - Click events 71 | - Close button 72 | - Filter selection 73 | - Disabled state 74 | 75 | 3. Test accessibility: 76 | 77 | - Keyboard navigation 78 | - ARIA attributes 79 | - Focus management 80 | 81 | 4. Example test: 82 | 83 | ```tsx 84 | describe('Chip', () => { 85 | it('handles close event', () => { 86 | const onClose = jest.fn(); 87 | render(); 88 | fireEvent.click(screen.getByRole('button")); 89 | expect(onClose).toHaveBeenCalled(); 90 | }); 91 | }); 92 | ``` 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multimodal-mcp-client", 3 | "version": "0.3.13", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "concurrently \"npm run proxy\" \"npm run dev:vite\"", 7 | "dev:vite": "vite", 8 | "proxy": "tsx --tsconfig proxy/tsconfig.json proxy/src/index.ts", 9 | "lint": "npm run lint:ts && npm run lint:md", 10 | "lint:ts": "eslint .", 11 | "test": "vitest run", 12 | "test:watch": "vitest watch", 13 | "test:coverage": "vitest run --coverage", 14 | "setup:google": "cd scripts/google-auth && npm install && npm start" 15 | }, 16 | "dependencies": { 17 | "@google/generative-ai": "^0.21.0", 18 | "@iconify/react": "^5.1.1", 19 | "@modelcontextprotocol/sdk": "^1.0.4", 20 | "@nextui-org/react": "^2.6.11", 21 | "@types/react-router-dom": "^5.3.3", 22 | "classnames": "^2.5.1", 23 | "cors": "^2.8.5", 24 | "dotenv": "^16.4.5", 25 | "eventemitter3": "^5.0.1", 26 | "eventsource": "^3.0.2", 27 | "express": "^4.18.2", 28 | "framer-motion": "^11.0.5", 29 | "lodash": "^4.17.21", 30 | "lucide-react": "^0.474.0", 31 | "next": "^14.1.0", 32 | "next-themes": "^0.4.4", 33 | "openapi-client-axios": "^7.5.5", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-router-dom": "^6.22.1", 37 | "shell-quote": "^1.8.1", 38 | "spawn-rx": "^5.1.1", 39 | "usehooks-ts": "^3.1.0", 40 | "zustand": "^4.5.1" 41 | }, 42 | "devDependencies": { 43 | "@eslint/js": "^8.56.0", 44 | "@testing-library/jest-dom": "^6.4.2", 45 | "@testing-library/react": "^14.2.1", 46 | "@testing-library/user-event": "^14.5.2", 47 | "@types/cors": "^2.8.17", 48 | "@types/eventsource": "^1.1.15", 49 | "@types/express": "^4.17.21", 50 | "@types/node": "^20.11.19", 51 | "@types/react": "^18.2.57", 52 | "@types/react-dom": "^18.2.19", 53 | "@types/shell-quote": "^1.7.5", 54 | "@vitejs/plugin-react": "^4.2.1", 55 | "@vitest/coverage-v8": "^1.2.2", 56 | "autoprefixer": "^10.4.17", 57 | "concurrently": "^8.2.2", 58 | "eslint": "^8.56.0", 59 | "eslint-plugin-react-hooks": "^4.6.0", 60 | "eslint-plugin-react-refresh": "^0.4.5", 61 | "globals": "^14.0.0", 62 | "jsdom": "^24.0.0", 63 | "postcss": "^8.4.35", 64 | "tailwindcss": "^3.4.1", 65 | "tsx": "^4.7.1", 66 | "typescript": "^5.3.3", 67 | "typescript-eslint": "^7.0.1", 68 | "vite": "^5.1.3", 69 | "vitest": "^1.2.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/features/server/hooks/__tests__/useModal.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from "@testing-library/react"; 2 | import { describe, it, expect } from "vitest"; 3 | import { useModal } from "../useModal"; 4 | 5 | describe("useModal", () => { 6 | it("should initialize with closed state", () => { 7 | const { result } = renderHook(() => useModal<{ name: string }>()); 8 | expect(result.current.viewModalOpen).toBe(false); 9 | expect(result.current.executeModalOpen).toBe(false); 10 | expect(result.current.selectedPrompt).toBeNull(); 11 | }); 12 | 13 | it("should open view modal with selected item", () => { 14 | const { result } = renderHook(() => useModal<{ name: string }>()); 15 | const testItem = { name: "test" }; 16 | 17 | act(() => { 18 | result.current.handleOpenViewModal(testItem); 19 | }); 20 | 21 | expect(result.current.viewModalOpen).toBe(true); 22 | expect(result.current.executeModalOpen).toBe(false); 23 | expect(result.current.selectedPrompt).toBe(testItem); 24 | }); 25 | 26 | it("should open execute modal with selected item", () => { 27 | const { result } = renderHook(() => useModal<{ name: string }>()); 28 | const testItem = { name: "test" }; 29 | 30 | act(() => { 31 | result.current.handleOpenExecuteModal(testItem); 32 | }); 33 | 34 | expect(result.current.executeModalOpen).toBe(true); 35 | expect(result.current.viewModalOpen).toBe(false); 36 | expect(result.current.selectedPrompt).toBe(testItem); 37 | }); 38 | 39 | it("should close modals and reset selected item", () => { 40 | const { result } = renderHook(() => useModal<{ name: string }>()); 41 | const testItem = { name: "test" }; 42 | 43 | // Open and close view modal 44 | act(() => { 45 | result.current.handleOpenViewModal(testItem); 46 | }); 47 | expect(result.current.viewModalOpen).toBe(true); 48 | 49 | act(() => { 50 | result.current.handleCloseViewModal(); 51 | }); 52 | expect(result.current.viewModalOpen).toBe(false); 53 | expect(result.current.selectedPrompt).toBeNull(); 54 | 55 | // Open and close execute modal 56 | act(() => { 57 | result.current.handleOpenExecuteModal(testItem); 58 | }); 59 | expect(result.current.executeModalOpen).toBe(true); 60 | 61 | act(() => { 62 | result.current.handleCloseExecuteModal(); 63 | }); 64 | expect(result.current.executeModalOpen).toBe(false); 65 | expect(result.current.selectedPrompt).toBeNull(); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/components/Modal/utils/form-state.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7, JSONSchema7Definition } from "json-schema"; 2 | 3 | export function getValueAtPath( 4 | obj: Record, 5 | path: string[] 6 | ): unknown { 7 | if (path.length === 0) return undefined; 8 | return path.reduce((current: unknown, key) => { 9 | if (current && typeof current === "object") { 10 | return (current as Record)[key]; 11 | } 12 | return undefined; 13 | }, obj); 14 | } 15 | 16 | export function setValueAtPath( 17 | obj: Record, 18 | path: string[], 19 | value: unknown 20 | ): Record { 21 | if (path.length === 0) return obj; 22 | 23 | const result = { ...obj }; 24 | let current = result; 25 | 26 | // Navigate to the parent of the target property 27 | for (let i = 0; i < path.length - 1; i++) { 28 | const key = path[i]; 29 | current[key] = current[key] 30 | ? { ...(current[key] as Record) } 31 | : {}; 32 | current = current[key] as Record; 33 | } 34 | 35 | // Set the value at the target property 36 | const lastKey = path[path.length - 1]; 37 | if (value === undefined) { 38 | delete current[lastKey]; 39 | } else { 40 | current[lastKey] = value; 41 | } 42 | 43 | return result; 44 | } 45 | 46 | export function getInitialValues( 47 | schema: JSONSchema7Definition 48 | ): Record { 49 | if (typeof schema === "boolean") return {}; 50 | 51 | const result: Record = {}; 52 | 53 | if (schema.oneOf) { 54 | // For oneOf schemas, initialize with the first option's type 55 | const firstOption = schema.oneOf[0]; 56 | if (typeof firstOption !== "boolean" && firstOption.properties?.type) { 57 | const typeSchema = firstOption.properties.type as JSONSchema7; 58 | if (typeSchema.const) { 59 | result.type = typeSchema.const; 60 | } 61 | } 62 | } 63 | 64 | if (schema.properties) { 65 | Object.entries(schema.properties).forEach(([key, propSchema]) => { 66 | if (typeof propSchema === "boolean") return; 67 | 68 | if (propSchema.type === "object") { 69 | result[key] = getInitialValues(propSchema); 70 | } else if (propSchema.type === "boolean") { 71 | result[key] = false; 72 | } else if (propSchema.enum && propSchema.enum.length > 0) { 73 | result[key] = propSchema.enum[0]; 74 | } else { 75 | result[key] = ""; 76 | } 77 | }); 78 | } 79 | 80 | return result; 81 | } 82 | -------------------------------------------------------------------------------- /src/components/Modal/components/FormField.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { Input, Select, SelectItem, Checkbox } from "@nextui-org/react"; 3 | import { JSONSchema7Definition } from "json-schema"; 4 | import { ValidationError } from "../utils/schema-utils"; 5 | 6 | interface FormFieldProps { 7 | schema: JSONSchema7Definition; 8 | path: string[]; 9 | value: unknown; 10 | error?: ValidationError; 11 | onChange: (path: string[], value: unknown) => void; 12 | label?: string; 13 | } 14 | 15 | function FormFieldComponent({ 16 | schema, 17 | path, 18 | value, 19 | error, 20 | onChange, 21 | label, 22 | }: FormFieldProps) { 23 | if (typeof schema === "boolean") return null; 24 | 25 | const fieldName = label || path[path.length - 1] || ""; 26 | 27 | // Handle boolean fields 28 | if (schema.type === "boolean") { 29 | return ( 30 | onChange(path, checked)} 33 | className="mb-4" 34 | > 35 | {schema.description || fieldName} 36 | 37 | ); 38 | } 39 | 40 | // Handle enum fields 41 | if (schema.enum) { 42 | const stringValue = value?.toString() || ""; 43 | return ( 44 | 61 | ); 62 | } 63 | 64 | // Handle basic input fields 65 | const stringValue = value?.toString() || ""; 66 | return ( 67 | { 72 | const newValue = 73 | schema.type === "number" ? Number(e.target.value) : e.target.value; 74 | onChange(path, newValue); 75 | }} 76 | errorMessage={error?.message} 77 | isInvalid={!!error} 78 | className="mb-4" 79 | data-testid={`tool-${fieldName}-input`} 80 | /> 81 | ); 82 | } 83 | 84 | export const FormField = memo(FormFieldComponent); 85 | -------------------------------------------------------------------------------- /src/components/Modal/components/DiscriminatorField.tsx: -------------------------------------------------------------------------------- 1 | import { Select, SelectItem } from "@nextui-org/react"; 2 | import { JSONSchema7Definition } from "json-schema"; 3 | import { ValidationError, getDiscriminatorSchema } from "../utils/schema-utils"; 4 | import { SchemaField } from "./SchemaField"; 5 | 6 | interface DiscriminatorFieldProps { 7 | schema: JSONSchema7Definition; 8 | path: string[]; 9 | value: unknown; 10 | error?: ValidationError; 11 | onChange: (path: string[], value: unknown) => void; 12 | } 13 | 14 | export function DiscriminatorField({ 15 | schema, 16 | path, 17 | value, 18 | error, 19 | onChange, 20 | }: DiscriminatorFieldProps) { 21 | if (typeof schema === "boolean") return null; 22 | 23 | const { discriminator, options } = getDiscriminatorSchema(schema); 24 | if (!discriminator?.enum) return null; 25 | 26 | const fieldName = path[path.length - 1] || ""; 27 | 28 | return ( 29 |
30 | 43 | {typeof value === "string" && ( 44 |
45 | {options.map((subSchema) => { 46 | if (typeof subSchema === "boolean") return null; 47 | if (!subSchema.properties?.type) return null; 48 | const typeSchema = subSchema.properties 49 | .type as JSONSchema7Definition; 50 | if (typeof typeSchema === "boolean") return null; 51 | if (typeSchema.const === value) { 52 | return Object.entries(subSchema.properties) 53 | .filter(([key]) => key !== "type") 54 | .map(([key, propSchema]) => ( 55 | 63 | )); 64 | } 65 | return null; 66 | })} 67 |
68 | )} 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Card/UserInfoCard.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Chip } from "@nextui-org/react"; 2 | import { BaseCard } from "./BaseCard"; 3 | 4 | interface UserInfoCardProps { 5 | userId: string; 6 | email: string; 7 | // apiKey: string; 8 | roles: string[]; 9 | onCopyApiKey: () => void; 10 | className?: string; 11 | } 12 | 13 | /** 14 | * UserInfoCard displays user information in a standardized card layout 15 | * @component 16 | * @example 17 | * ```tsx 18 | * handleCopy()} 24 | * /> 25 | * ``` 26 | */ 27 | export function UserInfoCard({ 28 | userId, 29 | email, 30 | // apiKey, 31 | roles, 32 | onCopyApiKey, 33 | className = "", 34 | }: UserInfoCardProps) { 35 | const details = [ 36 | { 37 | label: "User ID", 38 | value: userId, 39 | type: "monospace", 40 | }, 41 | { 42 | label: "Email", 43 | value: email, 44 | type: "monospace", 45 | }, 46 | // { 47 | // label: "API Key", 48 | // value: ( 49 | //
50 | // {apiKey.slice(0, 8)}... 51 | // 54 | //
55 | // ), 56 | // }, 57 | { 58 | label: "Roles", 59 | value: ( 60 |
61 | {roles.map((role) => ( 62 | 63 | {role} 64 | 65 | ))} 66 |
67 | ), 68 | }, 69 | ]; 70 | 71 | return ( 72 | 78 |
79 | {details.map(({ label, value, type }) => ( 80 |
81 |

{label}

82 | {typeof value === "string" ? ( 83 |

88 | {value} 89 |

90 | ) : ( 91 | value 92 | )} 93 |
94 | ))} 95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /proxy/src/services/McpApiService.ts: -------------------------------------------------------------------------------- 1 | import type { ServerConfig, TransformedMcpData } from "../types/index.js"; 2 | 3 | export class McpApiService { 4 | private readonly _baseUrl: string; 5 | private readonly apiKey: string; 6 | 7 | constructor(baseUrl: string, apiKey: string) { 8 | this._baseUrl = baseUrl; 9 | this.apiKey = apiKey; 10 | } 11 | 12 | // Getter for testing 13 | get baseUrl(): string { 14 | return this._baseUrl; 15 | } 16 | 17 | async fetchMcpData(): Promise { 18 | const response = await this.fetchFromApi("/v1/mcp"); 19 | return response; 20 | } 21 | 22 | async updateMcpConfig( 23 | config: TransformedMcpData 24 | ): Promise { 25 | const response = await this.postToApi( 26 | "/v1/mcp", 27 | config 28 | ); 29 | return response; 30 | } 31 | 32 | async transformServers( 33 | servers: Record 34 | ): Promise> { 35 | // Just return the servers as-is, no transformation needed 36 | return servers; 37 | } 38 | 39 | async fetchFromApi( 40 | endpoint: string, 41 | options: RequestInit = {} 42 | ): Promise { 43 | const url = new URL(endpoint, this._baseUrl); 44 | const response = await fetch(url, { 45 | ...options, 46 | headers: { 47 | ...options.headers, 48 | "Content-Type": "application/json", 49 | "api-key": this.apiKey, 50 | }, 51 | }); 52 | 53 | if (!response.ok) { 54 | console.error("API request failed:", { 55 | status: response.status, 56 | statusText: response.statusText, 57 | url: url.toString(), 58 | }); 59 | throw new Error(`API request failed: ${response.statusText}`); 60 | } 61 | 62 | return response.json(); 63 | } 64 | 65 | private async postToApi(endpoint: string, data: unknown): Promise { 66 | const url = new URL(endpoint, this._baseUrl).toString(); 67 | const response = await fetch(url, { 68 | method: "POST", 69 | headers: { 70 | "Content-Type": "application/json", 71 | "api-key": this.apiKey, 72 | }, 73 | body: JSON.stringify(data), 74 | }); 75 | 76 | if (!response.ok) { 77 | console.error("API request failed:", { 78 | status: response.status, 79 | statusText: response.statusText, 80 | url, 81 | }); 82 | throw new Error( 83 | `Error POSTing to endpoint (HTTP ${response.status}): ${await response.text()}` 84 | ); 85 | } 86 | 87 | return response.json(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/agent-registry-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # Agent Registry Feature 2 | 3 | ## Overview 4 | 5 | The Agent Registry is a core feature that manages and coordinates AI agents within the system. It provides functionality for storing, retrieving, and managing agent configurations, as well as handling the active agent state and associated tools. 6 | 7 | ## Directory Structure 8 | 9 | ``` 10 | 📁 agent-registry/ 11 | ├── 📁 __llm__/ 12 | │ └── 📄 README.md 13 | ├── 📁 contexts/ 14 | │ └── 📄 AgentRegistryContext.tsx 15 | ├── 📁 lib/ 16 | │ └── 📄 types.ts 17 | └── 📄 index.tsx 18 | ``` 19 | 20 | ## Key Components 21 | 22 | ### AgentRegistryContext 23 | 24 | A React context that provides agent management functionality throughout the application. It includes: 25 | 26 | - Agent state management 27 | - Tool management 28 | - Configuration handling 29 | - Active agent selection 30 | 31 | ### Types 32 | 33 | Located in `lib/types.ts`, defines the core interfaces: 34 | 35 | - `AgentConfig`: Configuration for individual agents 36 | - `PromptPost`: Structure for agent prompts 37 | - `AgentRegistryContextType`: Context interface 38 | 39 | ## Features 40 | 41 | ### Agent Management 42 | 43 | - Load agents from configuration 44 | - Save new agent configurations 45 | - Delete existing agents 46 | - Set and manage active agent 47 | - Retrieve agent details 48 | 49 | ### Tool Integration 50 | 51 | - Dynamic tool management with MCP (Model Context Protocol) 52 | - Tool state synchronization 53 | - Gemini format tool mapping 54 | 55 | ### Configuration 56 | 57 | Default configuration includes: 58 | 59 | - Model: "models/gemini-2.0-flash-exp" 60 | - Audio response capabilities 61 | - Voice configuration (default: "Kore") 62 | 63 | ## Usage 64 | 65 | ### Provider Setup 66 | 67 | ```tsx 68 | import { AgentRegistryProvider } from "./features/agent-registry"; 69 | 70 | function App() { 71 | return ( 72 | {/* Your app components */} 73 | ); 74 | } 75 | ``` 76 | 77 | ### Using the Hook 78 | 79 | ```tsx 80 | import { useAgentRegistry } from "./features/agent-registry"; 81 | 82 | function YourComponent() { 83 | const { agents, activeAgent, saveAgent, deleteAgent, setActiveAgent, tools } = 84 | useAgentRegistry(); 85 | 86 | // Use the context values and functions 87 | } 88 | ``` 89 | 90 | ## Integration Points 91 | 92 | - Integrates with MCP for tool management 93 | - Works with Gemini model for responses 94 | - Handles audio modality configurations 95 | - Manages system instructions and prompts 96 | 97 | ## Dependencies 98 | 99 | - React (Context, Hooks) 100 | - MCP SDK 101 | - Configuration utilities 102 | - Gemini model integration 103 | -------------------------------------------------------------------------------- /src/components/Layout/GridLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type GridColumns = 1 | 2 | 3 | 4 | 5 | 6; 4 | type GridGap = 2 | 3 | 4 | 5 | 6 | 8; 5 | type Breakpoint = "sm" | "md" | "lg" | "xl" | "2xl"; 6 | 7 | interface GridBreakpoint { 8 | cols?: GridColumns; 9 | gap?: GridGap; 10 | } 11 | 12 | export interface GridLayoutProps { 13 | children: React.ReactNode; 14 | className?: string; 15 | cols?: GridColumns; 16 | gap?: GridGap; 17 | responsive?: Partial>; 18 | } 19 | 20 | export function GridLayout({ 21 | children, 22 | className = "", 23 | cols = 1, 24 | gap = 4, 25 | responsive, 26 | }: GridLayoutProps) { 27 | const getResponsiveClasses = () => { 28 | if (!responsive) return ""; 29 | 30 | return Object.entries(responsive) 31 | .map(([breakpoint, config]) => { 32 | const classes = []; 33 | if (config.cols) { 34 | classes.push(`${breakpoint}:grid-cols-${config.cols}`); 35 | } 36 | if (config.gap) { 37 | classes.push(`${breakpoint}:gap-${config.gap}`); 38 | } 39 | return classes.join(" "); 40 | }) 41 | .join(" "); 42 | }; 43 | 44 | return ( 45 |
48 | {children} 49 |
50 | ); 51 | } 52 | 53 | // Preset layouts for common use cases 54 | export function TwoColumnLayout({ 55 | children, 56 | className = "", 57 | gap = 6, 58 | }: Omit) { 59 | return ( 60 | 68 | {children} 69 | 70 | ); 71 | } 72 | 73 | export function ThreeColumnLayout({ 74 | children, 75 | className = "", 76 | gap = 6, 77 | }: Omit) { 78 | return ( 79 | 88 | {children} 89 | 90 | ); 91 | } 92 | 93 | export function FourColumnLayout({ 94 | children, 95 | className = "", 96 | gap = 6, 97 | }: Omit) { 98 | return ( 99 | 109 | {children} 110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/shared/Button/BaseButton.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { Button as NextUIButton, ButtonProps } from "@nextui-org/react"; 3 | 4 | export interface BaseButtonProps extends ButtonProps { 5 | isLoading?: boolean; 6 | validationState?: { 7 | isValid: boolean; 8 | requiredFields: string[]; 9 | }; 10 | loadingText?: string; 11 | defaultText: string; 12 | } 13 | 14 | export function BaseButton({ 15 | isLoading, 16 | disabled, 17 | validationState, 18 | loadingText = "Loading...", 19 | defaultText, 20 | className = "", 21 | ...props 22 | }: BaseButtonProps) { 23 | const buttonState = useMemo(() => { 24 | if (isLoading) return { disabled: true, text: loadingText }; 25 | if (!validationState) return { disabled: disabled, text: defaultText }; 26 | 27 | const { isValid, requiredFields } = validationState; 28 | 29 | if (requiredFields.length > 0) { 30 | return { 31 | disabled: true, 32 | text: `Enter ${requiredFields.join(" & ")}`, 33 | }; 34 | } 35 | 36 | if (!isValid) { 37 | return { disabled: true, text: "Invalid Input" }; 38 | } 39 | 40 | return { disabled: disabled, text: defaultText }; 41 | }, [isLoading, validationState, disabled, loadingText, defaultText]); 42 | 43 | const isDisabled = buttonState.disabled; 44 | 45 | return ( 46 | 54 | {isLoading ? ( 55 |
56 | 77 | {loadingText} 78 |
79 | ) : null} 80 | 85 | {buttonState.text} 86 | 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /proxy/src/__tests__/handlers/configHandlers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 | import { ConfigHandlers } from "../../handlers/configHandlers.js"; 3 | import type { Request, Response } from "express"; 4 | import type { McpConfig } from "../../types/index.js"; 5 | 6 | describe("ConfigHandlers", () => { 7 | let handlers: ConfigHandlers; 8 | const mockConfig: McpConfig = { 9 | mcpServers: { 10 | default: { 11 | command: "test-command", 12 | args: ["--test"], 13 | }, 14 | }, 15 | customServers: { 16 | "custom-test": { 17 | command: "custom-command", 18 | args: ["--custom"], 19 | }, 20 | }, 21 | defaults: { 22 | serverTypes: { 23 | stdio: { 24 | icon: "test-icon", 25 | color: "primary", 26 | description: "Test stdio server", 27 | }, 28 | sse: { 29 | icon: "test-icon", 30 | color: "primary", 31 | description: "Test SSE server", 32 | }, 33 | }, 34 | unconnected: { 35 | icon: "test-icon", 36 | color: "secondary", 37 | description: "Test unconnected server", 38 | }, 39 | }, 40 | }; 41 | 42 | const originalEnv = process.env; 43 | 44 | const createMockResponse = () => ({ 45 | json: vi.fn(), 46 | status: vi.fn().mockReturnThis(), 47 | end: vi.fn(), 48 | }); 49 | 50 | beforeEach(() => { 51 | vi.clearAllMocks(); 52 | process.env = { ...originalEnv }; 53 | handlers = new ConfigHandlers(mockConfig); 54 | }); 55 | 56 | afterEach(() => { 57 | process.env = originalEnv; 58 | }); 59 | 60 | describe("handleConfig", () => { 61 | it("should return merged configuration with custom servers", async () => { 62 | const req = {} as Request; 63 | const res = createMockResponse(); 64 | 65 | handlers.handleConfig(req, res as unknown as Response); 66 | 67 | expect(res.status).toHaveBeenCalledWith(200); 68 | expect(res.json).toHaveBeenCalledWith({ 69 | mcpServers: { 70 | default: { 71 | command: "test-command", 72 | args: ["--test"], 73 | }, 74 | "custom-test": { 75 | command: "custom-command", 76 | args: ["--custom"], 77 | }, 78 | }, 79 | }); 80 | }); 81 | 82 | it("should handle missing configuration", async () => { 83 | handlers = new ConfigHandlers({} as McpConfig); 84 | const req = {} as Request; 85 | const res = createMockResponse(); 86 | 87 | handlers.handleConfig(req, res as unknown as Response); 88 | 89 | expect(res.status).toHaveBeenCalledWith(500); 90 | expect(res.json).toHaveBeenCalledWith({ 91 | error: "Invalid server configuration", 92 | }); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/stores/log-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { JSONSchema7 } from "json-schema"; 3 | 4 | export type LogType = "tool" | "prompt" | "multimodal" | "system"; 5 | 6 | export interface LogEntry { 7 | id: string; 8 | timestamp: number; 9 | type: LogType; 10 | operation: string; 11 | status: "success" | "error" | "info" | "warning"; 12 | name: string; 13 | params?: JSONSchema7; 14 | result?: unknown; 15 | error?: string; 16 | message?: string; 17 | count?: number; 18 | } 19 | 20 | interface LogStore { 21 | maxLogs: number; 22 | logs: LogEntry[]; 23 | addLog: (log: Omit) => void; 24 | clearLogs: (type?: LogType) => void; 25 | setMaxLogs: (n: number) => void; 26 | } 27 | 28 | const areDuplicateLogs = ( 29 | a: LogEntry, 30 | b: Omit 31 | ) => { 32 | return ( 33 | a.type === b.type && 34 | a.operation === b.operation && 35 | a.message === b.message && 36 | a.name === b.name && 37 | JSON.stringify(a.params) === JSON.stringify(b.params) 38 | ); 39 | }; 40 | 41 | export const useLogStore = create((set) => ({ 42 | maxLogs: 500, 43 | logs: [], 44 | addLog: (logEntry) => { 45 | set((state) => { 46 | const newLog: LogEntry = { 47 | ...logEntry, 48 | id: crypto.randomUUID(), 49 | timestamp: Date.now(), 50 | }; 51 | 52 | // Find any existing duplicate log 53 | const duplicateIndex = state.logs.findIndex((log) => 54 | areDuplicateLogs(log, logEntry) 55 | ); 56 | 57 | if (duplicateIndex !== -1) { 58 | // Remove the duplicate and add an updated version at the end 59 | const existingLog = state.logs[duplicateIndex]; 60 | const updatedLogs = [ 61 | ...state.logs.slice(0, duplicateIndex), 62 | ...state.logs.slice(duplicateIndex + 1), 63 | { 64 | ...existingLog, 65 | count: (existingLog.count || 1) + 1, 66 | timestamp: newLog.timestamp, 67 | }, 68 | ]; 69 | 70 | if (updatedLogs.length > state.maxLogs) { 71 | return { logs: updatedLogs.slice(1) }; 72 | } 73 | return { logs: updatedLogs }; 74 | } 75 | 76 | const updatedLogs = [...state.logs, newLog]; 77 | if (updatedLogs.length > state.maxLogs) { 78 | return { logs: updatedLogs.slice(1) }; 79 | } 80 | return { logs: updatedLogs }; 81 | }); 82 | }, 83 | clearLogs: (type?: LogType) => 84 | set((state) => ({ 85 | logs: type ? state.logs.filter((log) => log.type !== type) : [], 86 | })), 87 | setMaxLogs: (n: number) => 88 | set((state) => { 89 | if (state.logs.length <= n) { 90 | return { maxLogs: n }; 91 | } 92 | return { 93 | maxLogs: n, 94 | logs: state.logs.slice(state.logs.length - n), 95 | }; 96 | }), 97 | })); 98 | -------------------------------------------------------------------------------- /src/components/Layout/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface PageHeaderProps { 4 | title: React.ReactNode; 5 | subtitle?: React.ReactNode; 6 | icon?: React.ReactNode; 7 | action?: React.ReactNode; 8 | className?: string; 9 | } 10 | 11 | export function PageHeader({ 12 | title, 13 | subtitle, 14 | icon, 15 | action, 16 | className = "", 17 | }: PageHeaderProps) { 18 | return ( 19 |
22 |
23 |
24 | {icon} 25 | {typeof title === "string" ? ( 26 |

{title}

27 | ) : ( 28 | title 29 | )} 30 |
31 | {subtitle && 32 | (typeof subtitle === "string" ? ( 33 |

{subtitle}

34 | ) : ( 35 | subtitle 36 | ))} 37 |
38 | {action &&
{action}
} 39 |
40 | ); 41 | } 42 | 43 | export interface PageSectionProps { 44 | title?: React.ReactNode; 45 | subtitle?: React.ReactNode; 46 | action?: React.ReactNode; 47 | children: React.ReactNode; 48 | className?: string; 49 | contentClassName?: string; 50 | } 51 | 52 | export function PageSection({ 53 | title, 54 | subtitle, 55 | action, 56 | children, 57 | className = "", 58 | contentClassName = "", 59 | }: PageSectionProps) { 60 | return ( 61 |
62 | {(title || subtitle || action) && ( 63 |
64 |
65 | {title && 66 | (typeof title === "string" ? ( 67 |

{title}

68 | ) : ( 69 | title 70 | ))} 71 | {subtitle && 72 | (typeof subtitle === "string" ? ( 73 |

{subtitle}

74 | ) : ( 75 | subtitle 76 | ))} 77 |
78 | {action} 79 |
80 | )} 81 |
{children}
82 |
83 | ); 84 | } 85 | 86 | export interface PageLayoutProps { 87 | header?: React.ReactNode; 88 | children: React.ReactNode; 89 | className?: string; 90 | } 91 | 92 | export function PageLayout({ 93 | header, 94 | children, 95 | className = "", 96 | }: PageLayoutProps) { 97 | return ( 98 |
99 | {header} 100 | {children} 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /docs/logger-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # Logger Components 2 | 3 | ## Overview 4 | 5 | Logger components provide debugging and monitoring interfaces for displaying application logs and debug information. 6 | 7 | ## Available Components 8 | 9 | ### LogViewer 10 | 11 | Main component for displaying log entries with filtering and search. 12 | 13 | ```tsx 14 | {}} 18 | onLevelChange={(level) => {}} 19 | /> 20 | ``` 21 | 22 | ### LogEntry 23 | 24 | Individual log entry display with timestamp and level. 25 | 26 | ```tsx 27 | 33 | ``` 34 | 35 | ### ConsoleOutput 36 | 37 | Terminal-like console output display. 38 | 39 | ```tsx 40 | 41 | ``` 42 | 43 | ## Props 44 | 45 | ### LogViewer Props 46 | 47 | - `logs: LogEntry[]` - Array of log entries 48 | - `level?: LogLevel` - Current filter level 49 | - `onClear?: () => void` - Clear logs handler 50 | - `onLevelChange?: (level: LogLevel) => void` - Level filter handler 51 | - `className?: string` - Additional CSS classes 52 | - `maxHeight?: string | number` - Maximum height of viewer 53 | 54 | ### LogEntry Props 55 | 56 | - `timestamp: Date` - Entry timestamp 57 | - `level: 'debug' | 'info' | 'warn' | 'error'` - Log level 58 | - `message: string` - Log message 59 | - `details?: object` - Additional log details 60 | - `className?: string` - Additional CSS classes 61 | 62 | ### ConsoleOutput Props 63 | 64 | - `lines: string[]` - Output lines 65 | - `maxLines?: number` - Maximum lines to display 66 | - `autoScroll?: boolean` - Auto-scroll to bottom 67 | - `className?: string` - Additional CSS classes 68 | - `monospace?: boolean` - Use monospace font 69 | 70 | ## Testing Guidelines 71 | 72 | 1. Test log display: 73 | 74 | - Different log levels 75 | - Timestamp formatting 76 | - Message truncation 77 | - Details expansion 78 | 79 | 2. Test filtering and search: 80 | 81 | - Level filtering 82 | - Text search 83 | - Clear functionality 84 | - Max lines limit 85 | 86 | 3. Test accessibility: 87 | 88 | - Keyboard navigation 89 | - Screen reader support 90 | - Focus management 91 | - Color contrast for log levels 92 | 93 | 4. Example test: 94 | 95 | ```tsx 96 | describe("LogViewer", () => { 97 | it("filters logs by level", () => { 98 | const logs = [ 99 | { level: "error", message: "Error log" }, 100 | { level: "info", message: "Info log" }, 101 | ]; 102 | render(); 103 | expect(screen.getByText("Error log")).toBeInTheDocument(); 104 | expect(screen.queryByText("Info log")).not.toBeInTheDocument(); 105 | }); 106 | }); 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/button-language-models-guide.md: -------------------------------------------------------------------------------- 1 | # Button Components 2 | 3 | ## Overview 4 | 5 | Button components provide consistent, reusable button interfaces throughout the application. 6 | 7 | ## Available Components 8 | 9 | ### BaseButton 10 | 11 | The foundation button component with configurable styles and behaviors. 12 | 13 | ```tsx 14 | 23 | ``` 24 | 25 | ### IconButton 26 | 27 | Button variant optimized for icon-only interactions. 28 | 29 | ```tsx 30 | {}} /> 31 | ``` 32 | 33 | ### ConnectButton 34 | 35 | Specialized button for connection state management. 36 | 37 | ```tsx 38 | 44 | ``` 45 | 46 | ### RefreshButton 47 | 48 | Convenience button for refresh operations. 49 | 50 | ```tsx 51 | 52 | ``` 53 | 54 | ## Props 55 | 56 | ### BaseButton Props 57 | 58 | - `icon?: string` - Optional icon name 59 | - `iconPosition?: 'start' | 'end'` - Icon placement 60 | - `loading?: boolean` - Loading state 61 | - `loadingText?: string` - Text during loading 62 | - `color?: ButtonColor` - Button color variant 63 | - `size?: 'sm' | 'md' | 'lg'` - Button size 64 | - `disabled?: boolean` - Disabled state 65 | - `className?: string` - Additional CSS classes 66 | 67 | ### IconButton Props 68 | 69 | - `icon: string` - Required icon name 70 | - `label: string` - Accessibility label 71 | - `size?: 'sm' | 'md' | 'lg'` - Button size 72 | - `onClick?: () => void` - Click handler 73 | 74 | ### ConnectButton Props 75 | 76 | - `isConnected: boolean` - Connection state 77 | - `isConnecting: boolean` - Connecting state 78 | - `onConnect: () => void` - Connect handler 79 | - `onDisconnect: () => void` - Disconnect handler 80 | - `connectMessage?: string` - Custom connect text 81 | - `disconnectMessage?: string` - Custom disconnect text 82 | 83 | ## Testing Guidelines 84 | 85 | 1. Test all button states: 86 | 87 | - Default 88 | - Hover 89 | - Active 90 | - Disabled 91 | - Loading 92 | 93 | 2. Test accessibility: 94 | 95 | - Keyboard navigation 96 | - Screen reader compatibility 97 | - ARIA attributes 98 | 99 | 3. Test click handlers and state changes 100 | 101 | 4. Example test: 102 | 103 | ```tsx 104 | describe("Button", () => { 105 | it("handles click events", () => { 106 | const onClick = jest.fn(); 107 | render(); 108 | fireEvent.click(screen.getByText("Click Me")); 109 | expect(onClick).toHaveBeenCalled(); 110 | }); 111 | }); 112 | ``` 113 | -------------------------------------------------------------------------------- /src/features/llm-registry/contexts/LlmRegistryContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useContext, 4 | useState, 5 | useCallback, 6 | ReactNode, 7 | } from "react"; 8 | import { 9 | LlmProviderConfig, 10 | LlmProviderInstance, 11 | LlmRegistryContextType, 12 | } from "../lib/types"; 13 | 14 | const LlmRegistryContext = createContext(null); 15 | 16 | interface Props { 17 | children: ReactNode; 18 | } 19 | 20 | export function LlmRegistryProvider({ children }: Props) { 21 | const [providers, setProviders] = useState([]); 22 | const [instances, setInstances] = useState>( 23 | new Map() 24 | ); 25 | // Todo, add more providers 26 | const [activeProvider] = useState("gemini"); 27 | const [providerConfig] = useState>({ 28 | apiKey: "", 29 | model: "gemini-2.0-flash-exp", 30 | temperature: 0.7, 31 | maxTokens: 1000, 32 | }); 33 | 34 | const registerProvider = useCallback( 35 | (config: LlmProviderConfig, instance: LlmProviderInstance) => { 36 | setProviders((prev) => { 37 | if (prev.some((p) => p.id === config.id)) { 38 | return prev; 39 | } 40 | return [...prev, config]; 41 | }); 42 | 43 | setInstances((prev) => { 44 | const next = new Map(prev); 45 | next.set(config.id, instance); 46 | return next; 47 | }); 48 | }, 49 | [] 50 | ); 51 | 52 | const unregisterProvider = useCallback((providerId: string) => { 53 | setProviders((prev) => prev.filter((p) => p.id !== providerId)); 54 | setInstances((prev) => { 55 | const next = new Map(prev); 56 | next.delete(providerId); 57 | return next; 58 | }); 59 | }, []); 60 | 61 | const getProviderConfig = useCallback( 62 | (providerId: string): LlmProviderConfig | null => { 63 | const config = providers.find((p) => p.id === providerId); 64 | return config || null; 65 | }, 66 | [providers] 67 | ); 68 | 69 | const getProviderInstance = useCallback( 70 | (providerId: string): LlmProviderInstance | null => { 71 | const instance = instances.get(providerId); 72 | return instance || null; 73 | }, 74 | [instances] 75 | ); 76 | 77 | return ( 78 | 89 | {children} 90 | 91 | ); 92 | } 93 | 94 | export function useLlmRegistry() { 95 | const context = useContext(LlmRegistryContext); 96 | if (!context) { 97 | throw new Error("useLlmRegistry must be used within a LlmRegistryProvider"); 98 | } 99 | return context; 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Card/BaseCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | import { StatusCard } from "./StatusCard"; 4 | import { Spinner } from "@nextui-org/react"; 5 | 6 | interface BaseCardProps { 7 | icon?: string; 8 | iconClassName?: string; 9 | iconTestId?: string; 10 | title?: React.ReactNode; 11 | subtitle?: React.ReactNode; 12 | headerAction?: React.ReactNode; 13 | isLoading?: boolean; 14 | isEmpty?: boolean; 15 | error?: Error | null; 16 | emptyMessage?: string; 17 | className?: string; 18 | children?: React.ReactNode; 19 | } 20 | 21 | /** 22 | * BaseCard component that provides consistent card layout with loading, empty, and error states 23 | */ 24 | export function BaseCard({ 25 | icon, 26 | iconClassName = "text-primary", 27 | iconTestId, 28 | title, 29 | subtitle, 30 | headerAction, 31 | isLoading = false, 32 | isEmpty = false, 33 | error = null, 34 | emptyMessage = "No data available", 35 | className = "", 36 | children, 37 | }: BaseCardProps) { 38 | if (isLoading) { 39 | return ( 40 | 41 |
42 | 43 |
44 |
45 | ); 46 | } 47 | 48 | if (error) { 49 | return ( 50 | 57 | ); 58 | } 59 | 60 | if (isEmpty) { 61 | return ( 62 | 69 | ); 70 | } 71 | 72 | return ( 73 | 74 | {(title || headerAction) && ( 75 |
76 |
77 | {icon && ( 78 |
81 | 86 |
87 | )} 88 |
89 | {title &&

{title}

} 90 | {subtitle && ( 91 |

{subtitle}

92 | )} 93 |
94 |
95 | {headerAction &&
{headerAction}
} 96 |
97 | )} 98 |
{children}
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/Button/ConnectButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@nextui-org/react"; 2 | import { Icon } from "@iconify/react"; 3 | 4 | export interface ConnectButtonProps { 5 | isConnected: boolean; 6 | isConnecting?: boolean; 7 | onConnect: () => void; 8 | onDisconnect: () => void; 9 | connectMessage?: string; 10 | disconnectMessage?: string; 11 | connectingMessage?: string; 12 | size?: "sm" | "md" | "lg"; 13 | className?: string; 14 | disabled?: boolean; 15 | hasError?: boolean; 16 | } 17 | 18 | export function ConnectButton({ 19 | isConnected, 20 | isConnecting = false, 21 | onConnect, 22 | onDisconnect, 23 | connectMessage = "Connect", 24 | disconnectMessage = "Connected", 25 | connectingMessage = "Connecting", 26 | size = "md", 27 | className = "", 28 | disabled = false, 29 | hasError = false, 30 | }: ConnectButtonProps) { 31 | const getTypeStyles = () => { 32 | if (isConnecting) return "bg-default-50/50 border-default-200"; 33 | if (hasError) 34 | return "bg-danger-50/50 border-danger-200 hover:bg-danger-100/50"; 35 | if (isConnected) 36 | return "bg-success-50/50 border-success-200 hover:bg-success-100/50"; 37 | return "bg-default-50/50 border-default-200 hover:bg-default-100/50"; 38 | }; 39 | 40 | const getIcon = () => { 41 | if (isConnecting) return "solar:refresh-circle-line-duotone"; 42 | if (hasError) return "solar:shield-warning-line-duotone"; 43 | if (isConnected) return "solar:shield-check-line-duotone"; 44 | return "solar:power-line-duotone"; 45 | }; 46 | 47 | const getIconColor = () => { 48 | if (isConnecting) return "#f6933c"; 49 | if (hasError) return "var(--danger)"; 50 | if (isConnected) return "var(--success)"; 51 | return "#f6933c"; 52 | }; 53 | 54 | const getMessage = () => { 55 | if (isConnecting) return connectingMessage; 56 | if (hasError) return "Error"; 57 | if (isConnected) return disconnectMessage; 58 | return connectMessage; 59 | }; 60 | 61 | const getStatusDot = () => { 62 | if (hasError) return "bg-danger-500"; 63 | if (isConnected) return "bg-success-500 animate-pulse"; 64 | return "bg-default-500"; 65 | }; 66 | 67 | return ( 68 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/components/Modal/SamplingModal.tsx: -------------------------------------------------------------------------------- 1 | import { ContentModal } from "./ContentModal"; 2 | import type { 3 | CreateMessageRequest, 4 | CreateMessageResult, 5 | } from "@modelcontextprotocol/sdk/types.js"; 6 | import { useState } from "react"; 7 | import { useGlobalLlm } from "@/contexts/LlmProviderContext"; 8 | import { McpMeta } from "@/types/mcp"; 9 | 10 | interface SamplingModalProps { 11 | isOpen: boolean; 12 | onClose: () => void; 13 | request: CreateMessageRequest["params"]; 14 | onApprove: (response: CreateMessageResult) => void; 15 | onReject: () => void; 16 | 17 | serverId: string; 18 | } 19 | 20 | export function SamplingModal({ 21 | isOpen, 22 | onClose, 23 | request, 24 | onApprove, 25 | onReject, 26 | serverId, 27 | }: SamplingModalProps) { 28 | const llmProvider = useGlobalLlm(); 29 | const [isLoading, setIsLoading] = useState(false); 30 | // Explicitly mark serverId as intentionally unused 31 | void serverId; 32 | 33 | const handleSubmit = async () => { 34 | try { 35 | setIsLoading(true); 36 | // Execute with LLM provider 37 | const result = await llmProvider.executePrompt({ 38 | name: "sampling", 39 | messages: request.messages, 40 | params: { 41 | temperature: request.temperature, 42 | maxTokens: request.maxTokens, 43 | stopSequences: request.stopSequences, 44 | }, 45 | _meta: request._meta as McpMeta, 46 | }); 47 | 48 | const response: CreateMessageResult = { 49 | role: "assistant", 50 | content: { 51 | type: "text", 52 | text: result, 53 | }, 54 | model: "mcp-sampling", 55 | }; 56 | 57 | onApprove(response); 58 | } catch (error) { 59 | console.error("Failed to execute sampling:", error); 60 | } finally { 61 | setIsLoading(false); 62 | } 63 | }; 64 | 65 | const handleModalClose = () => { 66 | onReject(); 67 | onClose(); 68 | }; 69 | 70 | const sections = [ 71 | { 72 | title: "Messages", 73 | content: { 74 | messages: request.messages.map((msg) => ({ 75 | role: msg.role, 76 | text: "text" in msg.content ? msg.content.text : "", 77 | })), 78 | }, 79 | }, 80 | { 81 | title: "Parameters", 82 | content: { 83 | temperature: request.temperature, 84 | maxTokens: request.maxTokens, 85 | stopSequences: request.stopSequences?.join(", "), 86 | responseSchema: request._meta?.responseSchema, 87 | }, 88 | }, 89 | ]; 90 | 91 | return ( 92 | 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | import { useSidebarItems } from "../sidebar/SidebarItems"; 3 | import Sidebar from "../sidebar"; 4 | import ControlTray from "@/features/multimodal-agent/components/control-tray/ControlTray"; 5 | import { Icon } from "@iconify/react"; 6 | 7 | interface LayoutProps { 8 | children: ReactNode; 9 | } 10 | 11 | export default function Layout({ children }: LayoutProps) { 12 | const { sections, handleItemClick } = useSidebarItems(); 13 | const [isCollapsed, setIsCollapsed] = useState(false); 14 | 15 | return ( 16 |
17 | {/* Sidebar */} 18 | 79 | 80 | {/* Main Content */} 81 |
82 |
{children}
83 | 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /proxy/src/__tests__/transports/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { mapEnvironmentVariables } from "../../transports/index.js"; 3 | 4 | describe("mapEnvironmentVariables", () => { 5 | const mockBaseEnv = { 6 | SYSTEMPROMPT_API_KEY: "test-api-key", 7 | NOTION_API_KEY: "test-notion-key", 8 | OTHER_VAR: "other-value", 9 | }; 10 | 11 | it("should map array-style environment variables", () => { 12 | const envConfig = { 13 | "0": "SYSTEMPROMPT_API_KEY", 14 | "1": "NOTION_API_KEY", 15 | }; 16 | 17 | const result = mapEnvironmentVariables(envConfig, mockBaseEnv); 18 | 19 | expect(result.SYSTEMPROMPT_API_KEY).toBe("test-api-key"); 20 | expect(result.NOTION_API_KEY).toBe("test-notion-key"); 21 | expect(result.OTHER_VAR).toBe("other-value"); 22 | }); 23 | 24 | it("should handle direct key-value pairs", () => { 25 | const envConfig = { 26 | DIRECT_KEY: "direct-value", 27 | "0": "SYSTEMPROMPT_API_KEY", 28 | }; 29 | 30 | const result = mapEnvironmentVariables(envConfig, mockBaseEnv); 31 | 32 | expect(result.DIRECT_KEY).toBe("direct-value"); 33 | expect(result.SYSTEMPROMPT_API_KEY).toBe("test-api-key"); 34 | expect(result.OTHER_VAR).toBe("other-value"); 35 | }); 36 | 37 | it("should handle empty environment config", () => { 38 | const result = mapEnvironmentVariables({}, mockBaseEnv); 39 | expect(result).toEqual(mockBaseEnv); 40 | }); 41 | 42 | it("should handle undefined environment config", () => { 43 | const result = mapEnvironmentVariables(undefined, mockBaseEnv); 44 | expect(result).toEqual(mockBaseEnv); 45 | }); 46 | 47 | it("should handle missing environment variables", () => { 48 | const envConfig = { 49 | "0": "MISSING_VAR", 50 | "1": "SYSTEMPROMPT_API_KEY", 51 | }; 52 | 53 | const result = mapEnvironmentVariables(envConfig, mockBaseEnv); 54 | 55 | expect(result.MISSING_VAR).toBeUndefined(); 56 | expect(result.SYSTEMPROMPT_API_KEY).toBe("test-api-key"); 57 | expect(result.OTHER_VAR).toBe("other-value"); 58 | }); 59 | 60 | it("should handle non-string values", () => { 61 | const envConfig = { 62 | "0": "SYSTEMPROMPT_API_KEY", 63 | INVALID: 123, 64 | ALSO_INVALID: { key: "value" }, 65 | } as Record; 66 | 67 | const result = mapEnvironmentVariables(envConfig, mockBaseEnv); 68 | 69 | expect(result.SYSTEMPROMPT_API_KEY).toBe("test-api-key"); 70 | expect(result.INVALID).toBeUndefined(); 71 | expect(result.ALSO_INVALID).toBeUndefined(); 72 | expect(result.OTHER_VAR).toBe("other-value"); 73 | }); 74 | 75 | it("should handle process.env as default base environment", () => { 76 | const originalEnv = process.env; 77 | process.env = { ...originalEnv, TEST_VAR: "test-value" }; 78 | 79 | const envConfig = { 80 | "0": "TEST_VAR", 81 | }; 82 | 83 | const result = mapEnvironmentVariables(envConfig); 84 | expect(result.TEST_VAR).toBe("test-value"); 85 | 86 | process.env = originalEnv; 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/features/server/hooks/__tests__/usePromptLogger.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from "@testing-library/react"; 2 | import { describe, it, expect, vi, beforeEach } from "vitest"; 3 | import { usePromptLogger } from "../usePromptLogger"; 4 | import { useLogStore } from "@/stores/log-store"; 5 | 6 | vi.mock("@/stores/log-store"); 7 | 8 | describe("usePromptLogger", () => { 9 | const mockAddLog = vi.fn(); 10 | 11 | beforeEach(() => { 12 | vi.resetAllMocks(); 13 | (useLogStore as unknown as ReturnType).mockReturnValue({ 14 | addLog: mockAddLog, 15 | }); 16 | }); 17 | 18 | it("should log success with all parameters", () => { 19 | const { result } = renderHook(() => usePromptLogger()); 20 | const params = { param1: "value1" }; 21 | const logResult = { response: "test response" }; 22 | 23 | result.current.logSuccess("View Prompt", "testPrompt", params, logResult); 24 | 25 | expect(mockAddLog).toHaveBeenCalledWith({ 26 | type: "prompt", 27 | operation: "View Prompt", 28 | status: "success", 29 | name: "testPrompt", 30 | params, 31 | result: logResult, 32 | }); 33 | }); 34 | 35 | it("should log success without optional parameters", () => { 36 | const { result } = renderHook(() => usePromptLogger()); 37 | 38 | result.current.logSuccess("Execute Prompt", "testPrompt"); 39 | 40 | expect(mockAddLog).toHaveBeenCalledWith({ 41 | type: "prompt", 42 | operation: "Execute Prompt", 43 | status: "success", 44 | name: "testPrompt", 45 | }); 46 | }); 47 | 48 | it("should log error with Error object", () => { 49 | const { result } = renderHook(() => usePromptLogger()); 50 | const error = new Error("Test error"); 51 | const params = { param1: "value1" }; 52 | 53 | result.current.logError("View Prompt", "testPrompt", error, params); 54 | 55 | expect(mockAddLog).toHaveBeenCalledWith({ 56 | type: "prompt", 57 | operation: "View Prompt", 58 | status: "error", 59 | name: "testPrompt", 60 | params, 61 | error: "Test error", 62 | }); 63 | }); 64 | 65 | it("should log error with string message", () => { 66 | const { result } = renderHook(() => usePromptLogger()); 67 | const errorMessage = "Something went wrong"; 68 | 69 | result.current.logError("Execute Prompt", "testPrompt", errorMessage); 70 | 71 | expect(mockAddLog).toHaveBeenCalledWith({ 72 | type: "prompt", 73 | operation: "Execute Prompt", 74 | status: "error", 75 | name: "testPrompt", 76 | error: errorMessage, 77 | }); 78 | }); 79 | 80 | it("should log custom entry", () => { 81 | const { result } = renderHook(() => usePromptLogger()); 82 | const customEntry = { 83 | operation: "View Prompt" as const, 84 | status: "success" as const, 85 | name: "testPrompt", 86 | params: { param1: "value1" }, 87 | result: { response: "test" }, 88 | }; 89 | 90 | result.current.log(customEntry); 91 | 92 | expect(mockAddLog).toHaveBeenCalledWith({ 93 | type: "prompt", 94 | ...customEntry, 95 | }); 96 | }); 97 | }); 98 | --------------------------------------------------------------------------------