├── .gitignore ├── README.md ├── agent ├── .gitignore ├── .vscode │ ├── cspell.json │ └── settings.json ├── langgraph.json ├── math_server.py ├── poetry.lock ├── pyproject.toml ├── sample_agent │ ├── __init__.py │ └── agent.py └── uv.lock ├── app ├── api │ └── copilotkit │ │ └── route.ts ├── components │ ├── CopilotActionHandler.tsx │ ├── ExampleConfigs.tsx │ ├── MCPConfigForm.tsx │ ├── PreviewSpreadsheetChanges.tsx │ ├── SingleSpreadsheet.tsx │ ├── SpreadsheetRenderer.tsx │ └── ToolCallRenderer.tsx ├── favicon.ico ├── globals.css ├── hooks │ └── useLocalStorage.ts ├── instructions.ts ├── layout.tsx ├── page.tsx ├── types.ts └── utils │ └── canonicalSpreadsheetData.ts ├── components.json ├── eslint.config.mjs ├── lib └── utils.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── renovate.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # pnpm store 14 | /.pnpm-store/ 15 | 16 | # testing 17 | /coverage 18 | 19 | # next.js 20 | /.next/ 21 | /out/ 22 | 23 | # production 24 | /build 25 | 26 | # misc 27 | .DS_Store 28 | *.pem 29 | 30 | # debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | .pnpm-debug.log* 35 | 36 | # env files (can opt-in for committing if needed) 37 | .env* 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | next-env.d.ts 45 | 46 | # langgraph 47 | .langgraph_api 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Open MCP Client 4 | 5 | ![CopilotKit-Banner](https://github.com/user-attachments/assets/8167c845-0381-45d9-ad1c-83f995d48290) 6 |
7 | 8 | https://github.com/user-attachments/assets/364b6705-14d4-4e6d-bea7-fb9f12664fab 9 | 10 | 11 | # Getting Started 12 | 13 | ## Set Up Environment Variables 14 | Create a `.env` file at the root of your project: 15 | 16 | 17 | 18 | ```sh 19 | touch .env 20 | ``` 21 | 22 | Add the following to `.env`: 23 | 24 | 25 | ```sh 26 | LANGSMITH_API_KEY=lsv2_... 27 | OPENAI_API_KEY=sk-... 28 | ``` 29 | 30 | Next, navigate to the `agent` folder and create another `.env` file: 31 | 32 | 33 | 34 | ```sh 35 | cd agent 36 | touch .env 37 | ``` 38 | 39 | Add the following inside `agent/.env`: 40 | 41 | ```sh 42 | OPENAI_API_KEY=sk-... 43 | LANGSMITH_API_KEY=lsv2_... 44 | ``` 45 | 46 | ## Set Up Poetry: 47 | 48 | Poetry manages dependencies for the agent service. Install it with: 49 | 50 | 51 | ```sh 52 | pip install poetry 53 | ``` 54 | 55 | Verify the installation by running: 56 | 57 | 58 | ```sh 59 | poetry --version 60 | ``` 61 | 62 | ## Development 63 | 64 | For easier debugging, run the `frontend` and `agent` in separate terminals: 65 | 66 | 67 | ```bash 68 | # Terminal 1 - Frontend 69 | pnpm run dev-frontend 70 | 71 | # Terminal 2 - Agent 72 | pnpm run dev-agent 73 | ``` 74 | 75 | Alternatively, launch both services together: 76 | 77 | 78 | ```bash 79 | pnpm run dev 80 | ``` 81 | 82 | Visit [http://localhost:3000](http://localhost:3000) in your browser to view the application. 83 | 84 | 85 | ## Architecture 86 | 87 | The codebase is organized into two primary components: 88 | 89 | 90 | - **Frontend** - Handles the user interface. 91 | - **Agent** - Manages the core functionality. 92 | 93 | ## License 94 | Distributed under the MIT License. See LICENSE for more info. 95 | -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__/ 3 | *.pyc 4 | .env 5 | .vercel 6 | .langgraph_api -------------------------------------------------------------------------------- /agent/.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "langgraph", 6 | "langchain", 7 | "perplexity", 8 | "openai", 9 | "ainvoke", 10 | "pydantic", 11 | "tavily", 12 | "copilotkit", 13 | "fastapi", 14 | "uvicorn", 15 | "checkpointer", 16 | "dotenv" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /agent/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic" 3 | } 4 | -------------------------------------------------------------------------------- /agent/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "python_version": "3.12", 3 | "dockerfile_lines": [], 4 | "dependencies": ["."], 5 | "graphs": { 6 | "sample_agent": "./sample_agent/agent.py:graph" 7 | }, 8 | "env": ".env" 9 | } 10 | -------------------------------------------------------------------------------- /agent/math_server.py: -------------------------------------------------------------------------------- 1 | # math_server.py 2 | from mcp.server.fastmcp import FastMCP 3 | 4 | mcp = FastMCP("Math") 5 | 6 | @mcp.tool() 7 | def add(a: int, b: int) -> int: 8 | """Add two numbers""" 9 | return a + b 10 | 11 | @mcp.tool() 12 | def multiply(a: int, b: int) -> int: 13 | """Multiply two numbers""" 14 | return a * b 15 | 16 | if __name__ == "__main__": 17 | mcp.run(transport="stdio") -------------------------------------------------------------------------------- /agent/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "sample_agent" 3 | version = "0.1.0" 4 | description = "Starter" 5 | authors = ["Markus Ecker "] 6 | license = "MIT" 7 | 8 | [project] 9 | name = "sample_agent" 10 | version = "0.0.1" 11 | dependencies = [ 12 | "langchain-openai>=0.2.1", 13 | "langchain-anthropic>=0.2.1", 14 | "langchain>=0.3.1", 15 | "openai>=1.51.0", 16 | "langchain-community>=0.3.1", 17 | "copilotkit==0.1.39", 18 | "uvicorn>=0.31.0", 19 | "python-dotenv>=1.0.1", 20 | "langchain-core>=0.3.25", 21 | "langgraph-cli[inmem]>=0.1.64", 22 | "langchain-mcp-adapters>=0.0.3", 23 | "fastmcp>=0.4.1", 24 | "langgraph>=0.3.5" 25 | ] 26 | 27 | [build-system] 28 | requires = ["setuptools >= 61.0"] 29 | build-backend = "setuptools.build_meta" 30 | 31 | [tool.poetry.dependencies] 32 | python = ">=3.10,<3.13" 33 | langchain-openai = "^0.2.1" 34 | langchain-anthropic = "^0.2.1" 35 | langchain = "^0.3.1" 36 | openai = "^1.51.0" 37 | langchain-community = "^0.3.1" 38 | copilotkit = "0.1.39" 39 | uvicorn = "^0.31.0" 40 | python-dotenv = "^1.0.1" 41 | langchain-core = "^0.3.25" 42 | langgraph-cli = {extras = ["inmem"], version = "^0.1.64"} 43 | langchain-mcp-adapters = "^0.0.3" 44 | fastmcp = "^0.4.1" 45 | langgraph = "^0.3.5" 46 | 47 | [tool.poetry.scripts] 48 | demo = "sample_agent.demo:main" 49 | -------------------------------------------------------------------------------- /agent/sample_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CopilotKit/open-mcp-client/c8ea97b205ac860f08a4017dc36acba4d255b126/agent/sample_agent/__init__.py -------------------------------------------------------------------------------- /agent/sample_agent/agent.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the main entry point for the agent. 3 | It defines the workflow graph, state, tools, nodes and edges. 4 | """ 5 | 6 | from typing_extensions import Literal, TypedDict, Dict, List, Any, Union, Optional 7 | from langchain_openai import ChatOpenAI 8 | from langchain_core.runnables import RunnableConfig 9 | from langgraph.graph import StateGraph, END 10 | from langgraph.checkpoint.memory import MemorySaver 11 | from langgraph.types import Command 12 | from copilotkit import CopilotKitState 13 | from langchain_mcp_adapters.client import MultiServerMCPClient 14 | from langgraph.prebuilt import create_react_agent 15 | from copilotkit.langgraph import (copilotkit_exit) 16 | import os 17 | 18 | # Define the connection type structures 19 | class StdioConnection(TypedDict): 20 | command: str 21 | args: List[str] 22 | transport: Literal["stdio"] 23 | 24 | class SSEConnection(TypedDict): 25 | url: str 26 | transport: Literal["sse"] 27 | 28 | # Type for MCP configuration 29 | MCPConfig = Dict[str, Union[StdioConnection, SSEConnection]] 30 | 31 | class AgentState(CopilotKitState): 32 | """ 33 | Here we define the state of the agent 34 | 35 | In this instance, we're inheriting from CopilotKitState, which will bring in 36 | the CopilotKitState fields. We're also adding a custom field, `mcp_config`, 37 | which will be used to configure MCP services for the agent. 38 | """ 39 | # Define mcp_config as an optional field without skipping validation 40 | mcp_config: Optional[MCPConfig] 41 | 42 | # Default MCP configuration to use when no configuration is provided in the state 43 | # Uses relative paths that will work within the project structure 44 | DEFAULT_MCP_CONFIG: MCPConfig = { 45 | "math": { 46 | "command": "python", 47 | # Use a relative path that will be resolved based on the current working directory 48 | "args": [os.path.join(os.path.dirname(__file__), "..", "math_server.py")], 49 | "transport": "stdio", 50 | }, 51 | } 52 | 53 | async def chat_node(state: AgentState, config: RunnableConfig) -> Command[Literal["__end__"]]: 54 | """ 55 | This is a simplified agent that uses the ReAct agent as a subgraph. 56 | It handles both chat responses and tool execution in one node. 57 | """ 58 | # Get MCP configuration from state, or use the default config if not provided 59 | mcp_config = state.get("mcp_config", DEFAULT_MCP_CONFIG) 60 | 61 | print(f"mcp_config: {mcp_config}, default: {DEFAULT_MCP_CONFIG}") 62 | 63 | # Set up the MCP client and tools using the configuration from state 64 | async with MultiServerMCPClient(mcp_config) as mcp_client: 65 | # Get the tools 66 | mcp_tools = mcp_client.get_tools() 67 | 68 | # Create the react agent 69 | model = ChatOpenAI(model="gpt-4o") 70 | react_agent = create_react_agent(model, mcp_tools) 71 | 72 | # Prepare messages for the react agent 73 | agent_input = { 74 | "messages": state["messages"] 75 | } 76 | 77 | # Run the react agent subgraph with our input 78 | agent_response = await react_agent.ainvoke(agent_input) 79 | 80 | # Update the state with the new messages 81 | updated_messages = state["messages"] + agent_response.get("messages", []) 82 | await copilotkit_exit(config) 83 | # End the graph with the updated messages 84 | return Command( 85 | goto=END, 86 | update={"messages": updated_messages}, 87 | ) 88 | 89 | # Define the workflow graph with only a chat node 90 | workflow = StateGraph(AgentState) 91 | workflow.add_node("chat_node", chat_node) 92 | workflow.set_entry_point("chat_node") 93 | 94 | # Compile the workflow graph 95 | graph = workflow.compile(MemorySaver()) -------------------------------------------------------------------------------- /agent/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.13" 3 | 4 | [[package]] 5 | name = "sample-agent" 6 | version = "0.0.1" 7 | source = { editable = "." } 8 | -------------------------------------------------------------------------------- /app/api/copilotkit/route.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CopilotRuntime, 3 | copilotRuntimeNextJSAppRouterEndpoint, 4 | langGraphPlatformEndpoint, 5 | LangChainAdapter 6 | } from "@copilotkit/runtime"; 7 | import { NextRequest } from "next/server"; 8 | import { ChatOpenAI } from "@langchain/openai"; 9 | 10 | // You can use any service adapter here for multi-agent support. 11 | const serviceAdapter = new LangChainAdapter({ 12 | chainFn: async ({ messages, tools }) => { 13 | return model.bindTools(tools, { strict: true }).stream(messages); 14 | }, 15 | }) 16 | 17 | const model = new ChatOpenAI({ 18 | modelName: "gpt-4o-mini", 19 | temperature: 0, 20 | apiKey: process.env["OPENAI_API_KEY"], 21 | }); 22 | 23 | const runtime = new CopilotRuntime({ 24 | remoteEndpoints: [ 25 | langGraphPlatformEndpoint({ 26 | deploymentUrl: `${process.env.AGENT_DEPLOYMENT_URL || 'http://localhost:8123'}`, 27 | langsmithApiKey: process.env.LANGSMITH_API_KEY, 28 | agents: [ 29 | { 30 | name: 'sample_agent', 31 | description: 'A helpful LLM agent.', 32 | } 33 | ] 34 | }), 35 | ], 36 | }); 37 | 38 | export const POST = async (req: NextRequest) => { 39 | const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ 40 | runtime, 41 | serviceAdapter, 42 | endpoint: "/api/copilotkit", 43 | }); 44 | 45 | return handleRequest(req); 46 | }; -------------------------------------------------------------------------------- /app/components/CopilotActionHandler.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCopilotAction } from "@copilotkit/react-core"; 4 | import { ToolCallRenderer } from "../components/ToolCallRenderer"; 5 | 6 | /** 7 | * Client component that handles Copilot actions 8 | * This component has no UI of its own, it just sets up the action handler 9 | */ 10 | export const CopilotActionHandler: React.FC = () => { 11 | 12 | // add a custom action renderer for all actions 13 | useCopilotAction({ 14 | name: "*", 15 | render: ({ name, args, status, result }: any) => { 16 | return ( 17 | 23 | ); 24 | }, 25 | }); 26 | 27 | // Return null as this component doesn't render anything visible 28 | return null; 29 | }; -------------------------------------------------------------------------------- /app/components/ExampleConfigs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Calculator, 5 | Search, 6 | Database, 7 | ArrowRight, 8 | } from "lucide-react"; 9 | import { JSX } from "react"; 10 | 11 | type ExampleConfig = { 12 | name: string; 13 | description: string; 14 | config: Record; 15 | icon: JSX.Element; 16 | }; 17 | 18 | const EXAMPLE_CONFIGS: ExampleConfig[] = [ 19 | { 20 | name: "Math Service", 21 | description: 22 | "A simple Python server that can perform mathematical operations", 23 | icon: , 24 | config: { 25 | math: { 26 | command: "python", 27 | args: ["agent/math_server.py"], 28 | transport: "stdio", 29 | }, 30 | }, 31 | }, 32 | { 33 | name: "Web Search", 34 | description: "Connect to a search service via SSE", 35 | icon: , 36 | config: { 37 | search: { 38 | url: "http://localhost:8000/search/events", 39 | transport: "sse", 40 | }, 41 | }, 42 | }, 43 | { 44 | name: "Full Stack", 45 | description: 46 | "A combination of multiple services for comprehensive functionality", 47 | icon: , 48 | config: { 49 | math: { 50 | command: "python", 51 | args: ["agent/math_server.py"], 52 | transport: "stdio", 53 | }, 54 | search: { 55 | url: "http://localhost:8000/search/events", 56 | transport: "sse", 57 | }, 58 | database: { 59 | command: "node", 60 | args: ["scripts/db_server.js"], 61 | transport: "stdio", 62 | }, 63 | }, 64 | }, 65 | ]; 66 | 67 | interface ExampleConfigsProps { 68 | onSelectConfig: (config: Record) => void; 69 | } 70 | 71 | export function ExampleConfigs({ onSelectConfig }: ExampleConfigsProps) { 72 | return ( 73 |
74 |
75 | {EXAMPLE_CONFIGS.map((example) => ( 76 |
80 |
81 |
82 |
{example.icon}
83 |
84 |

{example.name}

85 |

{example.description}

86 |
87 |
88 | 95 |
96 |
97 |
 98 |                 {JSON.stringify(example.config, null, 2)}
 99 |               
100 |
101 |
102 | ))} 103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /app/components/MCPConfigForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { useCoAgent } from "@copilotkit/react-core"; 5 | import { ExampleConfigs } from "./ExampleConfigs"; 6 | import { useLocalStorage } from "../hooks/useLocalStorage"; 7 | 8 | type ConnectionType = "stdio" | "sse"; 9 | 10 | interface StdioConfig { 11 | command: string; 12 | args: string[]; 13 | transport: "stdio"; 14 | } 15 | 16 | interface SSEConfig { 17 | url: string; 18 | transport: "sse"; 19 | } 20 | 21 | type ServerConfig = StdioConfig | SSEConfig; 22 | 23 | // Define a generic type for our state 24 | interface AgentState { 25 | mcp_config: Record; 26 | } 27 | 28 | // Local storage key for saving agent state 29 | const STORAGE_KEY = "mcp-agent-state"; 30 | 31 | const ExternalLink = () => ( 32 | 39 | 45 | 46 | ); 47 | 48 | export function MCPConfigForm({ showSpreadsheet, setShowSpreadsheet }: { showSpreadsheet: boolean, setShowSpreadsheet: (value: boolean) => void }) { 49 | // Use our localStorage hook for persistent storage 50 | const [savedConfigs, setSavedConfigs] = useLocalStorage< 51 | Record 52 | >(STORAGE_KEY, {}); 53 | 54 | // Initialize agent state with the data from localStorage 55 | const { state: agentState, setState: setAgentState } = useCoAgent( 56 | { 57 | name: "sample_agent", 58 | initialState: { 59 | mcp_config: savedConfigs, 60 | }, 61 | } 62 | ); 63 | 64 | // Simple getter for configs 65 | const configs = agentState?.mcp_config || {}; 66 | 67 | // Simple setter wrapper for configs 68 | const setConfigs = (newConfigs: Record) => { 69 | setAgentState({ ...agentState, mcp_config: newConfigs }); 70 | setSavedConfigs(newConfigs); 71 | }; 72 | 73 | const [serverName, setServerName] = useState(""); 74 | const [connectionType, setConnectionType] = useState("stdio"); 75 | const [command, setCommand] = useState(""); 76 | const [args, setArgs] = useState(""); 77 | const [url, setUrl] = useState(""); 78 | const [isLoading, setIsLoading] = useState(true); 79 | const [showAddServerForm, setShowAddServerForm] = useState(false); 80 | const [showExampleConfigs, setShowExampleConfigs] = useState(false); 81 | 82 | // Calculate server statistics 83 | const totalServers = Object.keys(configs).length; 84 | const stdioServers = Object.values(configs).filter( 85 | (config) => config.transport === "stdio" 86 | ).length; 87 | const sseServers = Object.values(configs).filter( 88 | (config) => config.transport === "sse" 89 | ).length; 90 | 91 | // Set loading to false when state is loaded 92 | useEffect(() => { 93 | if (agentState) { 94 | setIsLoading(false); 95 | } 96 | }, [agentState]); 97 | 98 | const handleExampleConfig = (exampleConfig: Record) => { 99 | // Merge the example with existing configs or replace them based on user preference 100 | if (Object.keys(configs).length > 0) { 101 | const shouldReplace = window.confirm( 102 | "Do you want to replace your current configuration with this example? Click 'OK' to replace, or 'Cancel' to merge." 103 | ); 104 | 105 | if (shouldReplace) { 106 | setConfigs(exampleConfig); 107 | } else { 108 | setConfigs({ ...configs, ...exampleConfig }); 109 | } 110 | } else { 111 | setConfigs(exampleConfig); 112 | } 113 | 114 | // Close the examples panel after selection 115 | setShowExampleConfigs(false); 116 | }; 117 | 118 | const addConfig = () => { 119 | if (!serverName) return; 120 | 121 | const newConfig = 122 | connectionType === "stdio" 123 | ? { 124 | command, 125 | args: args.split(" ").filter((arg) => arg.trim() !== ""), 126 | transport: "stdio" as const, 127 | } 128 | : { 129 | url, 130 | transport: "sse" as const, 131 | }; 132 | 133 | setConfigs({ 134 | ...configs, 135 | [serverName]: newConfig, 136 | }); 137 | 138 | // Reset form 139 | setServerName(""); 140 | setCommand(""); 141 | setArgs(""); 142 | setUrl(""); 143 | setShowAddServerForm(false); 144 | }; 145 | 146 | const removeConfig = (name: string) => { 147 | const newConfigs = { ...configs }; 148 | delete newConfigs[name]; 149 | setConfigs(newConfigs); 150 | }; 151 | 152 | if (isLoading) { 153 | return
Loading configuration...
; 154 | } 155 | 156 | return ( 157 |
158 | {/* Header */} 159 |
160 |
161 |
162 | 169 | 175 | 176 |

177 | Open MCP Client 178 |

179 |
180 |
181 |
182 |
183 |

184 | Manage and configure your MCP servers 185 |

186 | 232 |
233 |
234 | 240 | 260 |
261 |
262 |
263 | 264 | {/* Server Statistics */} 265 |
266 |
267 |
Total Servers
268 |
{totalServers}
269 |
270 |
271 |
Stdio Servers
272 |
{stdioServers}
273 |
274 |
275 |
SSE Servers
276 |
{sseServers}
277 |
278 |
279 | 280 | {/* Example Configs Button */} 281 |
282 | 304 | 305 | {showExampleConfigs && ( 306 |
307 | 308 |
309 | )} 310 |
311 | 312 | {/* Server List */} 313 |
314 |

Server List

315 | 316 | {totalServers === 0 ? ( 317 |
318 | No servers configured. Click "Add Server" to get started. 319 |
320 | ) : ( 321 |
322 | {Object.entries(configs).map(([name, config]) => ( 323 |
327 |
328 |
329 |
330 |

{name}

331 |
332 | {config.transport === "stdio" ? ( 333 | 340 | 346 | 347 | ) : ( 348 | 355 | 361 | 362 | )} 363 | {config.transport} 364 |
365 |
366 | 385 |
386 |
387 | {config.transport === "stdio" ? ( 388 | <> 389 |

Command: {config.command}

390 |

391 | Args: {config.args.join(" ")} 392 |

393 | 394 | ) : ( 395 |

URL: {config.url}

396 | )} 397 |
398 |
399 |
400 | ))} 401 |
402 | )} 403 | 404 | {/* Composio & mcp.run reference */} 405 |
406 | More MCP servers available on the web, e.g.{" "} 407 | 413 | mcp.composio.dev 414 | 415 | 416 | and{" "} 417 | 423 | mcp.run 424 | 425 | 426 |
427 |
428 | 429 | {/* Add Server Modal */} 430 | {showAddServerForm && ( 431 |
432 |
433 |
434 |

435 | 442 | 448 | 449 | Add New Server 450 |

451 | 470 |
471 | 472 |
473 |
474 | 477 | setServerName(e.target.value)} 481 | className="w-full px-3 py-2 border rounded-md text-sm" 482 | placeholder="e.g., api-service, data-processor" 483 | /> 484 |
485 | 486 |
487 | 490 |
491 | 516 | 541 |
542 |
543 | 544 | {connectionType === "stdio" ? ( 545 | <> 546 |
547 | 550 | setCommand(e.target.value)} 554 | className="w-full px-3 py-2 border rounded-md text-sm" 555 | placeholder="e.g., python, node" 556 | /> 557 |
558 |
559 | 562 | setArgs(e.target.value)} 566 | className="w-full px-3 py-2 border rounded-md text-sm" 567 | placeholder="e.g., path/to/script.py" 568 | /> 569 |
570 | 571 | ) : ( 572 |
573 | 574 | setUrl(e.target.value)} 578 | className="w-full px-3 py-2 border rounded-md text-sm" 579 | placeholder="e.g., http://localhost:8000/events" 580 | /> 581 |
582 | )} 583 | 584 |
585 | 605 | 625 |
626 |
627 |
628 |
629 | )} 630 |
631 | ); 632 | } 633 | -------------------------------------------------------------------------------- /app/components/PreviewSpreadsheetChanges.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleIcon } from '@heroicons/react/20/solid' 2 | import { SpreadsheetRow } from '../types'; 3 | import { useState } from 'react'; 4 | import Spreadsheet from 'react-spreadsheet'; 5 | 6 | 7 | export interface PreviewSpreadsheetChanges { 8 | preCommitTitle: string; 9 | postCommitTitle: string; 10 | newRows: SpreadsheetRow[]; 11 | commit: (rows: SpreadsheetRow[]) => void; 12 | } 13 | 14 | export function PreviewSpreadsheetChanges(props: PreviewSpreadsheetChanges) { 15 | const [changesCommitted, setChangesCommitted] = useState(false); 16 | 17 | const commitChangesButton = () => { 18 | return ( 19 | 28 | ); 29 | } 30 | 31 | const changesCommittedButtonPlaceholder = () => { 32 | return ( 33 | 40 | ); 41 | } 42 | 43 | return ( 44 |
45 | 48 | 49 |
50 | {changesCommitted ? changesCommittedButtonPlaceholder() : commitChangesButton() } 51 |
52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/components/SingleSpreadsheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { 3 | useCopilotAction, 4 | useCopilotReadable, 5 | } from "@copilotkit/react-core"; 6 | import React from "react"; 7 | import Spreadsheet from "react-spreadsheet"; 8 | import { canonicalSpreadsheetData } from "../utils/canonicalSpreadsheetData"; 9 | import { SpreadsheetData, SpreadsheetRow } from "../types"; 10 | import { PreviewSpreadsheetChanges } from "./PreviewSpreadsheetChanges"; 11 | 12 | interface MainAreaProps { 13 | spreadsheet: SpreadsheetData; 14 | setSpreadsheet: (spreadsheet: SpreadsheetData) => void; 15 | } 16 | 17 | const SingleSpreadsheet = ({ spreadsheet, setSpreadsheet }: MainAreaProps) => { 18 | useCopilotReadable({ 19 | description: "The current spreadsheet", 20 | value: spreadsheet, 21 | }) 22 | 23 | useCopilotAction({ 24 | name: "suggestSpreadsheetOverride", 25 | description: "Suggest an override of the current spreadsheet", 26 | parameters: [ 27 | { 28 | name: "rows", 29 | type: "object[]", 30 | description: "The rows of the spreadsheet", 31 | attributes: [ 32 | { 33 | name: "cells", 34 | type: "object[]", 35 | description: "The cells of the row", 36 | attributes: [ 37 | { 38 | name: "value", 39 | type: "string", 40 | description: "The value of the cell", 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | { 47 | name: "title", 48 | type: "string", 49 | description: "The title of the spreadsheet", 50 | required: false, 51 | }, 52 | ], 53 | render: (props) => { 54 | const { rows } = props.args 55 | const newRows = canonicalSpreadsheetData(rows); 56 | 57 | return ( 58 | { 63 | const updatedSpreadsheet: SpreadsheetData = { 64 | title: spreadsheet.title, 65 | rows: rows, 66 | }; 67 | setSpreadsheet(updatedSpreadsheet); 68 | }} 69 | /> 70 | ) 71 | }, 72 | handler: ({ rows, title }) => { 73 | // Do nothing. 74 | // The preview component will optionally handle committing the changes. 75 | console.log(rows, title) 76 | }, 77 | }); 78 | 79 | useCopilotAction({ 80 | name: "appendToSpreadsheet", 81 | description: "Append rows to the current spreadsheet", 82 | parameters: [ 83 | { 84 | name: "rows", 85 | type: "object[]", 86 | description: "The new rows of the spreadsheet", 87 | attributes: [ 88 | { 89 | name: "cells", 90 | type: "object[]", 91 | description: "The cells of the row", 92 | attributes: [ 93 | { 94 | name: "value", 95 | type: "string", 96 | description: "The value of the cell", 97 | }, 98 | ], 99 | }, 100 | ], 101 | }, 102 | ], 103 | render: (props) => { 104 | const status = props.status; 105 | const { rows } = props.args 106 | const newRows = canonicalSpreadsheetData(rows); 107 | return ( 108 |
109 |

Status: {status}

110 | 113 |
114 | ) 115 | }, 116 | handler: ({ rows }) => { 117 | const canonicalRows = canonicalSpreadsheetData(rows); 118 | const updatedSpreadsheet: SpreadsheetData = { 119 | title: spreadsheet.title, 120 | rows: [...spreadsheet.rows, ...canonicalRows], 121 | }; 122 | setSpreadsheet(updatedSpreadsheet); 123 | }, 124 | }); 125 | 126 | return ( 127 |
128 | 133 | setSpreadsheet({ ...spreadsheet, title: e.target.value }) 134 | } 135 | /> 136 |
137 | { 140 | console.log("data", data); 141 | setSpreadsheet({ ...spreadsheet, rows: data as SpreadsheetRow[] }); 142 | }} 143 | /> 144 | 160 |
161 | 177 |
178 | ); 179 | }; 180 | 181 | export default SingleSpreadsheet; 182 | 183 | 184 | -------------------------------------------------------------------------------- /app/components/SpreadsheetRenderer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React,{useState} from "react" 3 | import { SpreadsheetData } from "../types"; 4 | import SingleSpreadsheet from "./SingleSpreadsheet"; 5 | const SpreadsheetRenderer=()=>{ 6 | const [spreadsheets, setSpreadsheets] = React.useState([ 7 | { 8 | title: "Spreadsheet 1", 9 | rows: [ 10 | [{ value: "" }, { value: "" }, { value: "" }], 11 | [{ value: "" }, { value: "" }, { value: "" }], 12 | [{ value: "" }, { value: "" }, { value: "" }], 13 | ], 14 | }, 15 | ]); 16 | // const [selectedSpreadsheetIndex, setSelectedSpreadsheetIndex] = useState(0); 17 | const [selectedSpreadsheetIndex] = useState(0); 18 | return ( 19 |
20 | { 23 | setSpreadsheets((prev) => { 24 | console.log("setSpreadsheet", spreadsheet); 25 | const newSpreadsheets = [...prev]; 26 | newSpreadsheets[selectedSpreadsheetIndex] = spreadsheet; 27 | return newSpreadsheets; 28 | }); 29 | }} 30 | /> 31 |
32 | ); 33 | } 34 | 35 | export default SpreadsheetRenderer 36 | -------------------------------------------------------------------------------- /app/components/ToolCallRenderer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | type ToolCallRendererProps = { 6 | name: string; 7 | args: any; 8 | status: string; 9 | result: any; 10 | }; 11 | 12 | export const ToolCallRenderer: React.FC = ({ 13 | name, 14 | args, 15 | status, 16 | result, 17 | }) => { 18 | const [isExpanded, setIsExpanded] = useState(false); 19 | 20 | const toggleExpand = () => { 21 | setIsExpanded(!isExpanded); 22 | }; 23 | 24 | // Format JSON objects for display 25 | const formatJSON = (obj: any) => { 26 | try { 27 | return JSON.stringify(obj, null, 2); 28 | } catch { 29 | return String(obj); 30 | } 31 | }; 32 | 33 | // Status color mapping 34 | const statusColors: Record = { 35 | running: "bg-yellow-100 text-yellow-800", 36 | success: "bg-green-100 text-green-800", 37 | error: "bg-red-100 text-red-800", 38 | pending: "bg-blue-100 text-blue-800", 39 | }; 40 | 41 | const statusColor = statusColors[status.toLowerCase()] || "bg-gray-100 text-gray-800"; 42 | 43 | return ( 44 |
45 | {/* Header - always visible */} 46 |
50 |
51 |
{name}
52 |
53 | {status} 54 |
55 |
56 | 69 |
70 | 71 | {/* Details - visible when expanded */} 72 | {isExpanded && ( 73 |
74 | {/* Arguments Section */} 75 |
76 |
Arguments:
77 |
78 |               {formatJSON(args)}
79 |             
80 |
81 | 82 | {/* Result Section - shown only if there's a result */} 83 | {result && ( 84 |
85 |
Result:
86 |
87 |                 {formatJSON(result)}
88 |               
89 |
90 | )} 91 |
92 | )} 93 |
94 | ); 95 | }; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CopilotKit/open-mcp-client/c8ea97b205ac860f08a4017dc36acba4d255b126/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "tailwindcss-animate"; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | @theme { 8 | --font-sans: var(--font-geist-sans); 9 | --font-mono: var(--font-geist-mono); 10 | } 11 | 12 | :root { 13 | --background: oklch(1 0 0); 14 | --foreground: oklch(0.141 0.005 285.823); 15 | --card: oklch(1 0 0); 16 | --card-foreground: oklch(0.141 0.005 285.823); 17 | --popover: oklch(1 0 0); 18 | --popover-foreground: oklch(0.141 0.005 285.823); 19 | --primary: oklch(0.21 0.006 285.885); 20 | --primary-foreground: oklch(0.985 0 0); 21 | --secondary: oklch(0.967 0.001 286.375); 22 | --secondary-foreground: oklch(0.21 0.006 285.885); 23 | --muted: oklch(0.967 0.001 286.375); 24 | --muted-foreground: oklch(0.552 0.016 285.938); 25 | --accent: oklch(0.967 0.001 286.375); 26 | --accent-foreground: oklch(0.21 0.006 285.885); 27 | --destructive: oklch(0.577 0.245 27.325); 28 | --destructive-foreground: oklch(0.577 0.245 27.325); 29 | --border: oklch(0.92 0.004 286.32); 30 | --input: oklch(0.92 0.004 286.32); 31 | --ring: oklch(0.871 0.006 286.286); 32 | --chart-1: oklch(0.646 0.222 41.116); 33 | --chart-2: oklch(0.6 0.118 184.704); 34 | --chart-3: oklch(0.398 0.07 227.392); 35 | --chart-4: oklch(0.828 0.189 84.429); 36 | --chart-5: oklch(0.769 0.188 70.08); 37 | --radius: 0.625rem;; 38 | --sidebar: oklch(0.985 0 0); 39 | --sidebar-foreground: oklch(0.141 0.005 285.823); 40 | --sidebar-primary: oklch(0.21 0.006 285.885); 41 | --sidebar-primary-foreground: oklch(0.985 0 0); 42 | --sidebar-accent: oklch(0.967 0.001 286.375); 43 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 44 | --sidebar-border: oklch(0.92 0.004 286.32); 45 | --sidebar-ring: oklch(0.871 0.006 286.286); 46 | } 47 | 48 | .dark { 49 | --background: oklch(0.141 0.005 285.823); 50 | --foreground: oklch(0.985 0 0); 51 | --card: oklch(0.141 0.005 285.823); 52 | --card-foreground: oklch(0.985 0 0); 53 | --popover: oklch(0.141 0.005 285.823); 54 | --popover-foreground: oklch(0.985 0 0); 55 | --primary: oklch(0.985 0 0); 56 | --primary-foreground: oklch(0.21 0.006 285.885); 57 | --secondary: oklch(0.274 0.006 286.033); 58 | --secondary-foreground: oklch(0.985 0 0); 59 | --muted: oklch(0.274 0.006 286.033); 60 | --muted-foreground: oklch(0.705 0.015 286.067); 61 | --accent: oklch(0.274 0.006 286.033); 62 | --accent-foreground: oklch(0.985 0 0); 63 | --destructive: oklch(0.396 0.141 25.723); 64 | --destructive-foreground: oklch(0.637 0.237 25.331); 65 | --border: oklch(0.274 0.006 286.033); 66 | --input: oklch(0.274 0.006 286.033); 67 | --ring: oklch(0.442 0.017 285.786); 68 | --chart-1: oklch(0.488 0.243 264.376); 69 | --chart-2: oklch(0.696 0.17 162.48); 70 | --chart-3: oklch(0.769 0.188 70.08); 71 | --chart-4: oklch(0.627 0.265 303.9); 72 | --chart-5: oklch(0.645 0.246 16.439); 73 | --sidebar: oklch(0.21 0.006 285.885); 74 | --sidebar-foreground: oklch(0.985 0 0); 75 | --sidebar-primary: oklch(0.488 0.243 264.376); 76 | --sidebar-primary-foreground: oklch(0.985 0 0); 77 | --sidebar-accent: oklch(0.274 0.006 286.033); 78 | --sidebar-accent-foreground: oklch(0.985 0 0); 79 | --sidebar-border: oklch(0.274 0.006 286.033); 80 | --sidebar-ring: oklch(0.442 0.017 285.786); 81 | } 82 | 83 | @theme inline { 84 | --color-background: var(--background); 85 | --color-foreground: var(--foreground); 86 | --color-card: var(--card); 87 | --color-card-foreground: var(--card-foreground); 88 | --color-popover: var(--popover); 89 | --color-popover-foreground: var(--popover-foreground); 90 | --color-primary: var(--primary); 91 | --color-primary-foreground: var(--primary-foreground); 92 | --color-secondary: var(--secondary); 93 | --color-secondary-foreground: var(--secondary-foreground); 94 | --color-muted: var(--muted); 95 | --color-muted-foreground: var(--muted-foreground); 96 | --color-accent: var(--accent); 97 | --color-accent-foreground: var(--accent-foreground); 98 | --color-destructive: var(--destructive); 99 | --color-destructive-foreground: var(--destructive-foreground); 100 | --color-border: var(--border); 101 | --color-input: var(--input); 102 | --color-ring: var(--ring); 103 | --color-chart-1: var(--chart-1); 104 | --color-chart-2: var(--chart-2); 105 | --color-chart-3: var(--chart-3); 106 | --color-chart-4: var(--chart-4); 107 | --color-chart-5: var(--chart-5); 108 | --radius-sm: calc(var(--radius) - 4px); 109 | --radius-md: calc(var(--radius) - 2px); 110 | --radius-lg: var(--radius); 111 | --radius-xl: calc(var(--radius) + 4px); 112 | --color-sidebar: var(--sidebar); 113 | --color-sidebar-foreground: var(--sidebar-foreground); 114 | --color-sidebar-primary: var(--sidebar-primary); 115 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 116 | --color-sidebar-accent: var(--sidebar-accent); 117 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 118 | --color-sidebar-border: var(--sidebar-border); 119 | --color-sidebar-ring: var(--sidebar-ring); 120 | } 121 | 122 | @layer base { 123 | * { 124 | @apply border-border outline-ring/50; 125 | } 126 | body { 127 | @apply bg-background text-foreground; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | 3 | export type SaveStatus = 'idle' | 'saving' | 'success' | 'error'; 4 | 5 | export function useLocalStorage( 6 | key: string, 7 | initialValue: T 8 | ): [T, (value: T) => void, SaveStatus] { 9 | // Function to get the initial value from localStorage 10 | const getInitialValue = (): T => { 11 | if (typeof window === 'undefined') return initialValue; 12 | 13 | try { 14 | const item = localStorage.getItem(key); 15 | // Check if the item exists in localStorage 16 | if (item) { 17 | return JSON.parse(item) as T; 18 | } 19 | // If not, return the initialValue and set it in localStorage 20 | localStorage.setItem(key, JSON.stringify(initialValue)); 21 | return initialValue; 22 | } catch (error) { 23 | console.error(`Error reading from localStorage with key "${key}":`, error); 24 | return initialValue; 25 | } 26 | }; 27 | 28 | // State to store our value and save status 29 | const [storedValue, setStoredValue] = useState(getInitialValue); 30 | const [saveStatus, setSaveStatus] = useState('idle'); 31 | 32 | // Ref to track if this is the first render 33 | const isFirstRender = useRef(true); 34 | 35 | // Save to localStorage whenever value changes (except on first render) 36 | useEffect(() => { 37 | // Skip saving on the first render 38 | if (isFirstRender.current) { 39 | isFirstRender.current = false; 40 | return; 41 | } 42 | 43 | try { 44 | setSaveStatus('saving'); 45 | localStorage.setItem(key, JSON.stringify(storedValue)); 46 | setSaveStatus('success'); 47 | 48 | // Reset status after delay 49 | const timer = setTimeout(() => setSaveStatus('idle'), 2000); 50 | return () => clearTimeout(timer); 51 | } catch (error) { 52 | console.error(`Error saving data to localStorage with key "${key}":`, error); 53 | setSaveStatus('error'); 54 | } 55 | }, [key, storedValue]); 56 | 57 | // Return a wrapped version of useState's setter function 58 | const setValue = (value: T) => { 59 | setSaveStatus('saving'); 60 | setStoredValue(value); 61 | }; 62 | 63 | return [storedValue, setValue, saveStatus]; 64 | } -------------------------------------------------------------------------------- /app/instructions.ts: -------------------------------------------------------------------------------- 1 | export const INSTRUCTIONS = ` 2 | All calculations will be performed by the sampleagent 3 | You assist the user with a spreadsheet. Basic formulas are permitted, for example \`=SUM(B1:B9)\`. 4 | When setting numbers, don't use commas. For example, use 1000 instead of 1,000. 5 | Don't use words like 'million' etc, just the numbers. 6 | 7 | When the user prompts you to create a new spreadsheet, focus on providing interesting numerical data and 8 | insert formulas when appropriate. 9 | 10 | If you have values that are a range, create a column for min and one for max. 11 | 12 | Available functions in formulas: 13 | ABS, ACOS, ACOSH, ACOT, ACOTH, ADDRESS, AND, ARABIC, AREAS, ASC, ASIN, ASINH, ATAN, ATAN2, ATANH, AVEDEV, AVERAGE, AVERAGEA, AVERAGEIF, BAHTTEXT, BASE, BESSELI, BESSELJ, BESSELK, BESSELY, BETA.DIST, BETA.INV, BIN2DEC, BIN2HEX, BIN2OCT, BINOM.DIST, BINOM.DIST.RANGE, BINOM.INV, BITAND, BITLSHIFT, BITOR, 14 | BITRSHIFT, BITXOR, CEILING, CEILING.MATH, CEILING.PRECISE, CHAR, CHISQ.DIST, CHISQ.DIST.RT, CHISQ.INV, CHISQ.INV.RT, CHISQ.TEST, CLEAN, CODE, COLUMN, COLUMNS, COMBIN, COMBINA, COMPLEX, CONCAT, CONCATENATE, CONFIDENCE.NORM, CONFIDENCE.T, CORREL, COS, COSH, COT, COTH, COUNT, COUNTIF, COVARIANCE.P, 15 | COVARIANCE.S, CSC, CSCH, DATE, DATEDIF, DATEVALUE, DAY, DAYS, DAYS360, DBCS, DEC2BIN, DEC2HEX, DEC2OCT, DECIMAL, DEGREES, DELTA, DEVSQ, DOLLAR, EDATE, ENCODEURL, EOMONTH, ERF, ERFC, ERROR.TYPE, EVEN, EXACT, EXP, EXPON.DIST, F.DIST, F.DIST.RT, F.INV, F.INV.RT, F.TEST, FACT, FACTDOUBLE, FALSE, FIND, FINDB, 16 | FISHER, FISHERINV, FIXED, FLOOR, FLOOR.MATH, FLOOR.PRECISE, FORECAST, FORECAST.LINEAR, FREQUENCY, GAMMA, GAMMA.DIST, GAMMA.INV, GAMMALN, GAMMALN.PRECISE, GAUSS, GCD, GEOMEAN, GESTEP, GROWTH, HARMEAN, HEX2BIN, HEX2DEC, HEX2OCT, HLOOKUP, HOUR, HYPGEOM.DIST, IF, IFERROR, IFNA, IFS, IMABS, IMAGINARY, IMARGUMENT, 17 | IMCONJUGATE, IMCOS, IMCOSH, IMCOT, IMCSC, IMCSCH, IMDIV, IMEXP, IMLN, IMLOG10, IMLOG2, IMPOWER, IMPRODUCT, IMREAL, IMSEC, IMSECH, IMSIN, IMSINH, IMSQRT, IMSUB, IMSUM, IMTAN, INDEX, INT, INTERCEPT, ISBLANK, ISERR, ISERROR, ISEVEN, ISLOGICAL, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISOWEEKNUM, ISREF, ISTEXT, 18 | KURT, LCM, LEFT, LEFTB, LN, LOG, LOG10, LOGNORM.DIST, LOGNORM.INV, LOWER, MDETERM, MID, MIDB, MINUTE, MMULT, MOD, MONTH, MROUND, MULTINOMIAL, MUNIT, N, NA, NEGBINOM.DIST, NETWORKDAYS, NETWORKDAYS.INTL, NORM.DIST, NORM.INV, NORM.S.DIST, NORM.S.INV, NOT, NOW, NUMBERVALUE, OCT2BIN, OCT2DEC, OCT2HEX, ODD, OR, 19 | PHI, PI, POISSON.DIST, POWER, PRODUCT, PROPER, QUOTIENT, RADIANS, RAND, RANDBETWEEN, REPLACE, REPLACEB, REPT, RIGHT, RIGHTB, ROMAN, ROUND, ROUNDDOWN, ROUNDUP, ROW, ROWS, SEARCH, SEARCHB, SEC, SECH, SECOND, SERIESSUM, SIGN, SIN, SINH, SQRT, SQRTPI, STANDARDIZE, SUM, SUMIF, SUMPRODUCT, SUMSQ, SUMX2MY2, 20 | SUMX2PY2, SUMXMY2, T, T.DIST, T.DIST.2T, T.DIST.RT, T.INV, T.INV.2T, TAN, TANH, TEXT, TIME, TIMEVALUE, TODAY, TRANSPOSE, TRIM, TRUE, TRUNC, TYPE, UNICHAR, UNICODE, UPPER, VLOOKUP, WEBSERVICE, WEEKDAY, WEEKNUM, WEIBULL.DIST, WORKDAY, WORKDAY.INTL, XOR, YEAR, YEARFRAC 21 | `; 22 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import "@copilotkit/react-ui/styles.css"; 5 | import { CopilotKit } from "@copilotkit/react-core"; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Open MCP Client", 19 | description: "An open source MCP client built with CopilotKit 🪁", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | 37 | {children} 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CopilotChat } from "@copilotkit/react-ui"; 4 | import { CopilotActionHandler } from "./components/CopilotActionHandler"; 5 | import { CopilotKitCSSProperties } from "@copilotkit/react-ui"; 6 | import { MCPConfigForm } from "./components/MCPConfigForm"; 7 | import { useState } from "react"; 8 | import SpreadsheetRenderer from "./components/SpreadsheetRenderer"; 9 | import { INSTRUCTIONS } from "./instructions"; 10 | 11 | export default function Home() { 12 | const [isChatOpen, setIsChatOpen] = useState(false); 13 | const [showSpreadsheet, setShowSpreadsheet] = useState(false); 14 | 15 | return ( 16 |
17 | {/* Client component that sets up the Copilot action handler */} 18 | 19 | 20 | {/* Main content area */} 21 |
22 | 24 | {showSpreadsheet && } 25 |
26 | {/* Mobile chat toggle button */} 27 | 64 | 65 | {/* Fixed sidebar - hidden on mobile, shown on larger screens */} 66 |
76 | 87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /app/types.ts: -------------------------------------------------------------------------------- 1 | export interface Cell { 2 | value: string; 3 | } 4 | 5 | export type SpreadsheetRow = Cell[]; 6 | 7 | export interface SpreadsheetData { 8 | title: string; 9 | rows: SpreadsheetRow[]; 10 | } 11 | -------------------------------------------------------------------------------- /app/utils/canonicalSpreadsheetData.ts: -------------------------------------------------------------------------------- 1 | import { SpreadsheetRow } from "../types"; 2 | 3 | export interface RowLike { 4 | cells: CellLike[] | undefined; 5 | } 6 | 7 | export interface CellLike { 8 | value: string; 9 | } 10 | 11 | export function canonicalSpreadsheetData( 12 | rows: RowLike[] | undefined 13 | ): SpreadsheetRow[] { 14 | const canonicalRows: SpreadsheetRow[] = []; 15 | 16 | for (const row of rows || []) { 17 | const canonicalRow: SpreadsheetRow = []; 18 | for (const cell of row.cells || []) { 19 | canonicalRow.push({value: cell.value}); 20 | } 21 | canonicalRows.push(canonicalRow); 22 | } 23 | 24 | return canonicalRows; 25 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | files: [ 16 | "app/components/CopilotActionHandler.tsx", 17 | "app/components/ExampleConfigs.tsx", 18 | "app/components/ToolCallRenderer.tsx", 19 | ], 20 | rules: { 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/ban-ts-comment": "off", 23 | }, 24 | }, 25 | ]; 26 | 27 | export default eslintConfig; 28 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whiteboard-canvas", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev-frontend": "pnpm i && next dev --turbopack", 7 | "dev-agent": "cd agent && poetry install && poetry run langgraph dev --host localhost --port 8123 --no-browser", 8 | "dev": "pnpx concurrently \"pnpm dev-frontend\" \"pnpm dev-agent\" --names ui,agent --prefix-colors blue,green", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@copilotkit/react-core": "^1.6.0", 15 | "@copilotkit/react-ui": "^1.6.0", 16 | "@copilotkit/runtime": "^1.6.0", 17 | "@heroicons/react": "^2.2.0", 18 | "@langchain/openai": "^0.4.5", 19 | "class-validator": "^0.14.1", 20 | "class-variance-authority": "^0.7.1", 21 | "clsx": "^2.1.1", 22 | "lucide-react": "^0.475.0", 23 | "next": "15.2.0-canary.73", 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0", 26 | "react-spreadsheet": "^0.9.5", 27 | "tailwind-merge": "^3.0.2", 28 | "tailwindcss-animate": "^1.0.7" 29 | }, 30 | "devDependencies": { 31 | "@eslint/eslintrc": "^3", 32 | "@tailwindcss/postcss": "^4", 33 | "@types/node": "^20", 34 | "@types/react": "^19", 35 | "@types/react-dom": "^19", 36 | "concurrently": "^9.1.2", 37 | "eslint": "^9", 38 | "eslint-config-next": "15.2.0-canary.73", 39 | "tailwindcss": "^4", 40 | "typescript": "^5" 41 | }, 42 | "packageManager": "pnpm@10.2.1" 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "dependencyDashboard": true, 4 | "extends": [ 5 | "config:recommended" 6 | ], 7 | "poetry": { 8 | "enabled": true 9 | }, 10 | "pep621": { 11 | "enabled": false 12 | }, 13 | "npm": { 14 | "enabled": true 15 | }, 16 | "ignorePaths": [ 17 | "node_modules" 18 | ], 19 | "packageRules": [ 20 | { 21 | "enabled": false, 22 | "matchPackageNames": [ 23 | "*" 24 | ], 25 | "labels": [ 26 | "dependencies" 27 | ] 28 | }, 29 | { 30 | "enabled": true, 31 | "matchPackageNames": [ 32 | "/^@copilotkit/", 33 | "/^copilotkit/" 34 | ], 35 | "labels": [ 36 | "copilotkit" 37 | ], 38 | "groupName": "CopilotKit dependencies" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------