diff --git a/src/app/(general)/_components/chat/messages/message-tool.tsx b/src/app/(general)/_components/chat/messages/message-tool.tsx index 9cfc7399..f606fb11 100644 --- a/src/app/(general)/_components/chat/messages/message-tool.tsx +++ b/src/app/(general)/_components/chat/messages/message-tool.tsx @@ -2,7 +2,7 @@ import { AnimatedShinyText } from "@/components/magicui/animated-shiny-text"; import { Card } from "@/components/ui/card"; import { HStack } from "@/components/ui/stack"; import { getClientToolkit } from "@/toolkits/toolkits/client"; -import type { Toolkits, ServerToolkitNames } from "@/toolkits/toolkits/shared"; +import type { Toolkits } from "@/toolkits/toolkits/shared"; import type { CreateMessage, DeepPartial, ToolInvocation } from "ai"; import { Loader2 } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; @@ -54,8 +54,17 @@ const MessageToolComponent: React.FC = ({ toolInvocation }) => { ); } - const typedTool = tool as ServerToolkitNames[typeof typedServer]; - const toolConfig = clientToolkit.tools[typedTool]; + const toolConfig = clientToolkit.tools[tool]; + + if (!toolConfig) { + return ( + + ); + } return ( = ({ toolInvocation }) => { ); }; +const FallbackToolInvocation: React.FC< + Props & { + icon: React.FC<{ className?: string }>; + toolkitName: string; + } +> = ({ toolInvocation, icon: Icon, toolkitName }) => { + const argsDefined = toolInvocation.args !== undefined; + const isRunning = + toolInvocation.state === "call" || toolInvocation.state === "partial-call"; + + return ( + + + + {toolkitName} Toolkit + {isRunning && } + +
+ {argsDefined && ( +
+            {JSON.stringify(toolInvocation.args, null, 2)}
+          
+ )} + {toolInvocation.state === "result" && ( +
+            {JSON.stringify(toolInvocation.result, null, 2)}
+          
+ )} +
+
+ ); +}; + const areEqual = (prevProps: Props, nextProps: Props): boolean => { const { toolInvocation: prev } = prevProps; const { toolInvocation: next } = nextProps; diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 2b858f04..a780a9d2 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -158,9 +158,8 @@ export async function POST(request: Request) { toolkits.map(async ({ id, parameters }) => { const toolkit = getServerToolkit(id); const tools = await toolkit.tools(parameters); - return Object.keys(tools).reduce( - (acc, toolName) => { - const serverTool = tools[toolName as keyof typeof tools]; + return Object.entries(tools).reduce( + (acc, [toolName, serverTool]) => { acc[`${id}_${toolName}`] = tool({ description: serverTool.description, parameters: serverTool.inputSchema, diff --git a/src/toolkits/toolkits/client.ts b/src/toolkits/toolkits/client.ts index 29087577..efc45b56 100644 --- a/src/toolkits/toolkits/client.ts +++ b/src/toolkits/toolkits/client.ts @@ -19,6 +19,7 @@ import { spotifyClientToolkit } from "./spotify/client"; import { etsyClientToolkit } from "./etsy/client"; import { videoClientToolkit } from "./video/client"; import { twitterClientToolkit } from "./twitter/client"; +import { mcpClientToolkit } from "./mcp/client"; export type ClientToolkits = { [K in Toolkits]: ClientToolkit< @@ -42,6 +43,7 @@ export const clientToolkits: ClientToolkits = { [Toolkits.Etsy]: etsyClientToolkit, [Toolkits.Video]: videoClientToolkit, [Toolkits.Twitter]: twitterClientToolkit, + [Toolkits.Mcp]: mcpClientToolkit, }; export function getClientToolkit( diff --git a/src/toolkits/toolkits/mcp/base.ts b/src/toolkits/toolkits/mcp/base.ts new file mode 100644 index 00000000..23664832 --- /dev/null +++ b/src/toolkits/toolkits/mcp/base.ts @@ -0,0 +1,47 @@ +import type { BaseTool, ToolkitConfig } from "@/toolkits/types"; +import { z } from "zod"; + +const headersSchema = z + .string() + .optional() + .refine( + (headers) => { + const trimmedHeaders = headers?.trim(); + if (!trimmedHeaders) return true; + + try { + const parsed: unknown = JSON.parse(trimmedHeaders); + + return ( + !!parsed && + !Array.isArray(parsed) && + typeof parsed === "object" && + Object.values(parsed).every((value) => typeof value === "string") + ); + } catch { + return false; + } + }, + { + message: "Headers must be a JSON object with string values.", + }, + ); + +export const mcpParameters = z.object({ + serverUrl: z + .string() + .trim() + .url() + .refine((value) => new URL(value).protocol === "https:", { + message: "MCP server URL must use HTTPS.", + }), + headers: headersSchema, +}); + +export const baseMcpToolkitConfig: ToolkitConfig< + string, + typeof mcpParameters.shape +> = { + tools: {} as Record, + parameters: mcpParameters, +}; diff --git a/src/toolkits/toolkits/mcp/client.tsx b/src/toolkits/toolkits/mcp/client.tsx new file mode 100644 index 00000000..4400bd97 --- /dev/null +++ b/src/toolkits/toolkits/mcp/client.tsx @@ -0,0 +1,23 @@ +import { Cable } from "lucide-react"; + +import { createClientToolkit } from "@/toolkits/create-toolkit"; +import { ToolkitGroups } from "@/toolkits/types"; + +import { baseMcpToolkitConfig } from "./base"; +import { Form } from "./form"; + +export const mcpClientToolkit = createClientToolkit< + string, + typeof baseMcpToolkitConfig.parameters.shape +>( + baseMcpToolkitConfig, + { + name: "Hosted MCP", + description: "Connect tools from a hosted MCP server", + icon: Cable, + form: Form, + type: ToolkitGroups.DataSource, + envVars: [], + }, + {}, +); diff --git a/src/toolkits/toolkits/mcp/form.tsx b/src/toolkits/toolkits/mcp/form.tsx new file mode 100644 index 00000000..59629b94 --- /dev/null +++ b/src/toolkits/toolkits/mcp/form.tsx @@ -0,0 +1,52 @@ +"use client"; + +import type React from "react"; +import type { z, ZodObject } from "zod"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { VStack } from "@/components/ui/stack"; + +import type { mcpParameters } from "./base"; + +export const Form: React.ComponentType<{ + parameters: z.infer>; + setParameters: ( + parameters: z.infer>, + ) => void; +}> = ({ parameters, setParameters }) => { + return ( + +
+ + + setParameters({ + ...parameters, + serverUrl: event.target.value, + }) + } + /> +
+
+ +