mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-09 19:45:17 +02:00
allow importing mcp tools
This commit is contained in:
parent
51166c19b4
commit
5dca666879
15 changed files with 1584 additions and 165 deletions
82
apps/rowboat/app/actions/mcp_actions.ts
Normal file
82
apps/rowboat/app/actions/mcp_actions.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use server";
|
||||
import { z } from "zod";
|
||||
import { WorkflowTool } from "../lib/types/workflow_types";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { projectAuthCheck } from "./project_actions";
|
||||
import { callMcpTool } from "../lib/utils";
|
||||
import { projectsCollection } from "../lib/mongodb";
|
||||
import { Project } from "../lib/types/project_types";
|
||||
|
||||
export async function fetchMcpTools(projectId: string): Promise<z.infer<typeof WorkflowTool>[]> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
const project = await projectsCollection.findOne({
|
||||
_id: projectId,
|
||||
});
|
||||
|
||||
const mcpServers = project?.mcpServers ?? [];
|
||||
|
||||
const tools: z.infer<typeof WorkflowTool>[] = [];
|
||||
|
||||
for (const mcpServer of mcpServers) {
|
||||
try {
|
||||
const transport = new SSEClientTransport(new URL(mcpServer.url));
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "rowboat-client",
|
||||
version: "1.0.0"
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// List tools
|
||||
const result = await client.listTools();
|
||||
|
||||
await client.close();
|
||||
|
||||
tools.push(...result.tools.map((mcpTool) => {
|
||||
let props = mcpTool.inputSchema.properties as Record<string, { description: string; type: string }>;
|
||||
const tool: z.infer<typeof WorkflowTool> = {
|
||||
name: mcpTool.name,
|
||||
description: mcpTool.description ?? "",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: props ?? {},
|
||||
required: mcpTool.inputSchema.required as string[] ?? [],
|
||||
},
|
||||
isMcp: true,
|
||||
mcpServerName: mcpServer.name,
|
||||
}
|
||||
return tool;
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(`Error fetching MCP tools from ${mcpServer.name}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
export async function updateMcpServers(projectId: string, mcpServers: z.infer<typeof Project>['mcpServers']): Promise<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
await projectsCollection.updateOne({
|
||||
_id: projectId,
|
||||
}, { $set: { mcpServers } });
|
||||
}
|
||||
|
||||
export async function executeMcpTool(projectId: string, mcpServerName: string, toolName: string, parameters: Record<string, unknown>): Promise<unknown> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
const result = await callMcpTool(projectId, mcpServerName, toolName, parameters);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { ObjectId } from "mongodb";
|
|||
import { authCheck } from "../../utils";
|
||||
import { ApiRequest, ApiResponse } from "../../../../lib/types/types";
|
||||
import { AgenticAPIChatRequest, AgenticAPIChatMessage, convertFromAgenticApiToApiMessages, convertFromApiToAgenticApiMessages, convertWorkflowToAgenticAPI } from "../../../../lib/types/agents_api_types";
|
||||
import { getAgenticApiResponse, callClientToolWebhook, runRAGToolCall, mockToolResponse } from "../../../../lib/utils";
|
||||
import { getAgenticApiResponse, callClientToolWebhook, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../lib/utils";
|
||||
import { check_query_limit } from "../../../../lib/rate_limiting";
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
import { PrefixLogger } from "../../../../lib/utils";
|
||||
|
|
@ -162,7 +162,7 @@ export async function POST(
|
|||
return Response.json({ error: "Error running RAG tool call" }, { status: 500 });
|
||||
}
|
||||
} else {
|
||||
logger.log(`Running client tool webhook for tool ${toolCall.function.name}`);
|
||||
logger.log(`Processing tool call ${toolCall.function.name}`);
|
||||
|
||||
try {
|
||||
// if tool is supposed to be mocked, mock it
|
||||
|
|
@ -170,6 +170,10 @@ export async function POST(
|
|||
if (testProfile?.mockTools || workflowTool?.mockTool) {
|
||||
logger.log(`Mocking tool call ${toolCall.function.name}`);
|
||||
result = await mockToolResponse(toolCall.id, currentMessages, testProfile?.mockPrompt || workflowTool?.mockInstructions || '');
|
||||
} else if (workflowTool?.isMcp) {
|
||||
// else run the tool call by calling the MCP tool
|
||||
logger.log(`Calling MCP tool: ${toolCall.function.name}`);
|
||||
result = await callMcpTool(projectId, workflowTool.mcpServerName ?? 'default', toolCall.function.name, JSON.parse(toolCall.function.arguments));
|
||||
} else {
|
||||
// else run the tool call by calling the client tool webhook
|
||||
logger.log(`Running client tool webhook for tool ${toolCall.function.name}`);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { convertFromAgenticAPIChatMessages } from "../../../../../../lib/types/a
|
|||
import { convertToAgenticAPIChatMessages } from "../../../../../../lib/types/agents_api_types";
|
||||
import { convertWorkflowToAgenticAPI } from "../../../../../../lib/types/agents_api_types";
|
||||
import { AgenticAPIChatRequest } from "../../../../../../lib/types/agents_api_types";
|
||||
import { callClientToolWebhook, getAgenticApiResponse, runRAGToolCall, mockToolResponse } from "../../../../../../lib/utils";
|
||||
import { callClientToolWebhook, getAgenticApiResponse, runRAGToolCall, mockToolResponse, callMcpTool } from "../../../../../../lib/utils";
|
||||
import { check_query_limit } from "../../../../../../lib/rate_limiting";
|
||||
import { PrefixLogger } from "../../../../../../lib/utils";
|
||||
|
||||
|
|
@ -158,14 +158,22 @@ export async function POST(
|
|||
[...messages, ...unsavedMessages],
|
||||
workflowTool.mockInstructions || ''
|
||||
);
|
||||
} else if (workflowTool?.isMcp) {
|
||||
logger.log(`Calling MCP tool: ${toolCall.function.name}`);
|
||||
return await callMcpTool(
|
||||
session.projectId,
|
||||
workflowTool.mcpServerName ?? 'default',
|
||||
toolCall.function.name,
|
||||
JSON.parse(toolCall.function.arguments)
|
||||
);
|
||||
} else {
|
||||
logger.log(`Calling webhook for tool: ${toolCall.function.name}`);
|
||||
return await callClientToolWebhook(
|
||||
toolCall,
|
||||
[...messages, ...unsavedMessages],
|
||||
session.projectId,
|
||||
);
|
||||
}
|
||||
|
||||
logger.log(`Calling webhook for tool: ${toolCall.function.name}`);
|
||||
return await callClientToolWebhook(
|
||||
toolCall,
|
||||
[...messages, ...unsavedMessages],
|
||||
session.projectId,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.log(`Error executing tool call ${toolCall.id}: ${error}`);
|
||||
return { error: "Tool execution failed" };
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export function ListItem({
|
|||
onClick,
|
||||
disabled,
|
||||
rightElement,
|
||||
selectedRef
|
||||
selectedRef,
|
||||
icon
|
||||
}: {
|
||||
name: string;
|
||||
isSelected: boolean;
|
||||
|
|
@ -31,6 +32,7 @@ export function ListItem({
|
|||
disabled?: boolean;
|
||||
rightElement?: React.ReactNode;
|
||||
selectedRef?: React.RefObject<HTMLButtonElement>;
|
||||
icon?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
|
|
@ -41,9 +43,12 @@ export function ListItem({
|
|||
"hover:bg-gray-50 dark:hover:bg-gray-800": !isSelected,
|
||||
})}
|
||||
>
|
||||
<div className={clsx("truncate text-sm dark:text-gray-200", {
|
||||
"text-gray-400 dark:text-gray-500": disabled,
|
||||
})}>{name}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{icon && <div className="w-4 shrink-0">{icon}</div>}
|
||||
<div className={clsx("truncate text-sm dark:text-gray-200", {
|
||||
"text-gray-400 dark:text-gray-500": disabled,
|
||||
})}>{name}</div>
|
||||
</div>
|
||||
{rightElement}
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const Project = z.object({
|
||||
_id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
|
|
@ -11,16 +12,22 @@ export const Project = z.object({
|
|||
publishedWorkflowId: z.string().optional(),
|
||||
nextWorkflowNumber: z.number().optional(),
|
||||
testRunCounter: z.number().default(0),
|
||||
});export const ProjectMember = z.object({
|
||||
mcpServers: z.array(z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
})).optional(),
|
||||
});
|
||||
|
||||
export const ProjectMember = z.object({
|
||||
userId: z.string(),
|
||||
projectId: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
lastUpdatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const ApiKey = z.object({
|
||||
projectId: z.string(),
|
||||
key: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
lastUsedAt: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -45,6 +45,8 @@ export const WorkflowTool = z.object({
|
|||
})),
|
||||
required: z.array(z.string()).optional(),
|
||||
}),
|
||||
isMcp: z.boolean().default(false).optional(),
|
||||
mcpServerName: z.string().optional(),
|
||||
});
|
||||
export const Workflow = z.object({
|
||||
name: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,54 @@ import { qdrantClient } from "./qdrant";
|
|||
import { EmbeddingRecord } from "./types/datasource_types";
|
||||
import { ApiMessage } from "./types/types";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { TestProfile } from "./types/testing_types";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
|
||||
export async function callMcpTool(
|
||||
projectId: string,
|
||||
mcpServerName: string,
|
||||
toolName: string,
|
||||
parameters: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const project = await projectsCollection.findOne({
|
||||
"_id": projectId,
|
||||
});
|
||||
if (!project) {
|
||||
throw new Error('Project not found');
|
||||
}
|
||||
|
||||
const mcpServer = project.mcpServers?.find(s => s.name === mcpServerName);
|
||||
if (!mcpServer) {
|
||||
throw new Error('MCP server not found');
|
||||
}
|
||||
|
||||
const transport = new SSEClientTransport(new URL(mcpServer.url));
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "rowboat-client",
|
||||
version: "1.0.0"
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: parameters,
|
||||
});
|
||||
|
||||
await client.close();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function callClientToolWebhook(
|
||||
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number],
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import { Metadata } from "next";
|
||||
import { Spinner, Textarea, Button, Dropdown, DropdownMenu, DropdownItem, DropdownTrigger, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure, Divider } from "@heroui/react";
|
||||
import { ReactNode, useEffect, useState, useCallback } from "react";
|
||||
import { ReactNode, useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../actions/project_actions";
|
||||
import { updateMcpServers } from "../../../actions/mcp_actions";
|
||||
import { CopyButton } from "../../../lib/components/copy-button";
|
||||
import { EditableField } from "../../../lib/components/editable-field";
|
||||
import { EyeIcon, EyeOffIcon, CopyIcon, MoreVerticalIcon, PlusIcon, EllipsisVerticalIcon } from "lucide-react";
|
||||
|
|
@ -113,6 +114,227 @@ export function BasicSettingsSection({
|
|||
</Section>;
|
||||
}
|
||||
|
||||
function McpServersSection({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
const [servers, setServers] = useState<Array<{ name: string; url: string }>>([]);
|
||||
const [originalServers, setOriginalServers] = useState<Array<{ name: string; url: string }>>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [newServer, setNewServer] = useState({ name: '', url: '' });
|
||||
const [validationErrors, setValidationErrors] = useState<{
|
||||
name?: string;
|
||||
url?: string;
|
||||
}>({});
|
||||
|
||||
// Load initial servers
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
getProjectConfig(projectId).then((project) => {
|
||||
const initialServers = project.mcpServers || [];
|
||||
setServers(JSON.parse(JSON.stringify(initialServers))); // Deep copy
|
||||
setOriginalServers(JSON.parse(JSON.stringify(initialServers))); // Deep copy
|
||||
setLoading(false);
|
||||
});
|
||||
}, [projectId]);
|
||||
|
||||
// Check if there are unsaved changes by comparing the arrays
|
||||
const hasChanges = useMemo(() => {
|
||||
if (servers.length !== originalServers.length) return true;
|
||||
return servers.some((server, index) => {
|
||||
return server.name !== originalServers[index]?.name ||
|
||||
server.url !== originalServers[index]?.url;
|
||||
});
|
||||
}, [servers, originalServers]);
|
||||
|
||||
const handleAddServer = () => {
|
||||
setNewServer({ name: '', url: '' });
|
||||
setValidationErrors({});
|
||||
onOpen();
|
||||
};
|
||||
|
||||
const handleRemoveServer = (index: number) => {
|
||||
setServers(servers.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleCreateServer = () => {
|
||||
// Clear previous validation errors
|
||||
setValidationErrors({});
|
||||
|
||||
const errors: typeof validationErrors = {};
|
||||
|
||||
// Validate name uniqueness
|
||||
if (!newServer.name.trim()) {
|
||||
errors.name = 'Server name is required';
|
||||
} else if (servers.some(s => s.name === newServer.name)) {
|
||||
errors.name = 'Server name must be unique';
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
if (!newServer.url.trim()) {
|
||||
errors.url = 'Server URL is required';
|
||||
} else {
|
||||
try {
|
||||
new URL(newServer.url);
|
||||
} catch {
|
||||
errors.url = 'Invalid URL format';
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setValidationErrors(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
setServers([...servers, newServer]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateMcpServers(projectId, servers);
|
||||
setOriginalServers(JSON.parse(JSON.stringify(servers))); // Update original servers after successful save
|
||||
setMessage({ type: 'success', text: 'Servers updated successfully' });
|
||||
setTimeout(() => setMessage(null), 3000);
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: 'Failed to update servers' });
|
||||
}
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
return <Section title="MCP servers">
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
MCP servers are used to execute MCP tools.
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
startContent={<PlusIcon className="w-4 h-4" />}
|
||||
onPress={handleAddServer}
|
||||
>
|
||||
Add Server
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<Spinner size="sm" />
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{servers.map((server, index) => (
|
||||
<div key={index} className="flex gap-3 items-center p-3 border border-border rounded-md">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{server.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{server.url}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
variant="light"
|
||||
onPress={() => handleRemoveServer(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{servers.length === 0 && (
|
||||
<div className="text-center text-muted-foreground p-4">
|
||||
No servers configured
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasChanges && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onPress={handleSave}
|
||||
isLoading={saving}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div className={`text-sm p-2 rounded-md ${
|
||||
message.type === 'success' ? 'bg-green-50 text-green-500' : 'bg-red-50 text-red-500'
|
||||
}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>Add MCP Server</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
label="Server Name"
|
||||
placeholder="Enter server name"
|
||||
value={newServer.name}
|
||||
onChange={(e) => {
|
||||
setNewServer({ ...newServer, name: e.target.value });
|
||||
// Clear name error when user types
|
||||
if (validationErrors.name) {
|
||||
setValidationErrors(prev => ({
|
||||
...prev,
|
||||
name: undefined
|
||||
}));
|
||||
}
|
||||
}}
|
||||
errorMessage={validationErrors.name}
|
||||
isInvalid={!!validationErrors.name}
|
||||
isRequired
|
||||
/>
|
||||
<Input
|
||||
label="SSE URL"
|
||||
placeholder="https://localhost:8000/sse"
|
||||
value={newServer.url}
|
||||
onChange={(e) => {
|
||||
setNewServer({ ...newServer, url: e.target.value });
|
||||
// Clear URL error when user types
|
||||
if (validationErrors.url) {
|
||||
setValidationErrors(prev => ({
|
||||
...prev,
|
||||
url: undefined
|
||||
}));
|
||||
}
|
||||
}}
|
||||
errorMessage={validationErrors.url}
|
||||
isInvalid={!!validationErrors.url}
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleCreateServer}
|
||||
isDisabled={!newServer.name || !newServer.url}
|
||||
>
|
||||
Add Server
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</Section>;
|
||||
}
|
||||
|
||||
function ApiKeyDisplay({ apiKey }: { apiKey: string }) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
|
|
@ -587,6 +809,7 @@ export default function App({
|
|||
<BasicSettingsSection projectId={projectId} />
|
||||
<SecretSection projectId={projectId} />
|
||||
<ApiKeysSection projectId={projectId} />
|
||||
<McpServersSection projectId={projectId} />
|
||||
<WebhookUrlSection projectId={projectId} />
|
||||
{useChatWidget && <ChatWidgetSection projectId={projectId} chatWidgetHost={chatWidgetHost} />}
|
||||
<DeleteProjectSection projectId={projectId} />
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import z from "zod";
|
||||
import { Workflow } from "../../../lib/types/workflow_types";
|
||||
import { WorkflowTool } from "../../../lib/types/workflow_types";
|
||||
import { WebpageCrawlResponse } from "../../../lib/types/tool_types";
|
||||
import { GetInformationToolResult } from "../../../lib/types/tool_types";
|
||||
import { executeClientTool, getInformationTool, scrapeWebpage, suggestToolResponse } from "../../../actions/actions";
|
||||
import { executeClientTool, getInformationTool, suggestToolResponse } from "../../../actions/actions";
|
||||
import MarkdownContent from "../../../lib/components/markdown-content";
|
||||
import Link from "next/link";
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
import { EditableField } from "../../../lib/components/editable-field";
|
||||
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronsDownIcon, ChevronsRightIcon, ChevronRightIcon, ChevronDownIcon, ExternalLinkIcon, XIcon } from "lucide-react";
|
||||
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||
import { executeMcpTool } from "@/app/actions/mcp_actions";
|
||||
|
||||
function UserMessage({ content }: { content: string }) {
|
||||
return <div className="self-end ml-[30%] flex flex-col">
|
||||
|
|
@ -201,6 +200,17 @@ function ToolCall({
|
|||
systemMessage={systemMessage}
|
||||
/>;
|
||||
}
|
||||
if (matchingWorkflowTool?.isMcp) {
|
||||
return <McpToolCall
|
||||
toolCall={toolCall}
|
||||
workflowTool={matchingWorkflowTool}
|
||||
result={result}
|
||||
handleResult={handleResult}
|
||||
projectId={projectId}
|
||||
messages={messages}
|
||||
sender={sender}
|
||||
/>;
|
||||
}
|
||||
return <ClientToolCall
|
||||
toolCall={toolCall}
|
||||
result={result}
|
||||
|
|
@ -345,6 +355,78 @@ function TransferToAgentToolCall({
|
|||
</div>;
|
||||
}
|
||||
|
||||
function McpToolCall({
|
||||
toolCall,
|
||||
result: availableResult,
|
||||
handleResult,
|
||||
projectId,
|
||||
messages,
|
||||
sender,
|
||||
workflowTool,
|
||||
}: {
|
||||
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
|
||||
result: z.infer<typeof apiV1.ToolMessage> | undefined;
|
||||
handleResult: (result: z.infer<typeof apiV1.ToolMessage>) => void;
|
||||
projectId: string;
|
||||
messages: z.infer<typeof apiV1.ChatMessage>[];
|
||||
sender: string | null | undefined;
|
||||
workflowTool: z.infer<typeof WorkflowTool>;
|
||||
}) {
|
||||
const [result, setResult] = useState<z.infer<typeof apiV1.ToolMessage> | undefined>(availableResult);
|
||||
|
||||
useEffect(() => {
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
let ignore = false;
|
||||
|
||||
async function process() {
|
||||
let response;
|
||||
try {
|
||||
response = await executeMcpTool(
|
||||
projectId,
|
||||
workflowTool.mcpServerName || '',
|
||||
workflowTool.name,
|
||||
JSON.parse(toolCall.function.arguments),
|
||||
);
|
||||
} catch (e) {
|
||||
response = {
|
||||
error: (e as Error).message,
|
||||
};
|
||||
}
|
||||
if (ignore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result: z.infer<typeof apiV1.ToolMessage> = {
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id,
|
||||
tool_name: toolCall.function.name,
|
||||
content: JSON.stringify(response),
|
||||
};
|
||||
setResult(result);
|
||||
handleResult(result);
|
||||
}
|
||||
process();
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [result, toolCall, projectId, messages, handleResult, workflowTool.mcpServerName, workflowTool.name]);
|
||||
|
||||
return <div className="flex flex-col gap-1">
|
||||
{sender && <div className='text-gray-500 text-sm ml-3'>{sender}</div>}
|
||||
<div className='border border-gray-300 p-2 pt-2 rounded-lg rounded-bl-none flex flex-col gap-2 mr-[30%]'>
|
||||
<ToolCallHeader toolCall={toolCall} result={result} />
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
|
||||
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ClientToolCall({
|
||||
toolCall,
|
||||
result: availableResult,
|
||||
|
|
@ -407,8 +489,8 @@ function ClientToolCall({
|
|||
<ToolCallHeader toolCall={toolCall} result={result} />
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={Boolean(!result)} />
|
||||
{result && <ExpandableContent label='Result' content={result.content} expanded={true} />}
|
||||
<ExpandableContent label='Params' content={toolCall.function.arguments} expanded={false} />
|
||||
{result && <ExpandableContent label='Result' content={result.content} expanded={false} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/r
|
|||
import { useRef, useEffect } from "react";
|
||||
import { ActionButton, StructuredPanel } from "../../../lib/components/structured-panel";
|
||||
import clsx from "clsx";
|
||||
import { EllipsisVerticalIcon } from "lucide-react";
|
||||
import { EllipsisVerticalIcon, ImportIcon } from "lucide-react";
|
||||
import { SectionHeader, ListItem } from "../../../lib/components/structured-list";
|
||||
|
||||
interface EntityListProps {
|
||||
|
|
@ -100,6 +100,7 @@ export function EntityList({
|
|||
onClick={() => onSelectTool(tool.name)}
|
||||
selectedRef={selectedEntity?.type === "tool" && selectedEntity.name === tool.name ? selectedRef : undefined}
|
||||
rightElement={<EntityDropdown name={tool.name} onDelete={onDeleteTool} />}
|
||||
icon={tool.isMcp ? <ImportIcon className="w-4 h-4 text-blue-700" /> : <></>}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
|||
151
apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx
Normal file
151
apps/rowboat/app/projects/[projectId]/workflow/mcp_imports.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"use client";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Spinner, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Checkbox } from "@heroui/react";
|
||||
import { z } from "zod";
|
||||
import { WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { RefreshCwIcon } from "lucide-react";
|
||||
import { fetchMcpTools } from "@/app/actions/mcp_actions";
|
||||
|
||||
interface McpImportToolsProps {
|
||||
projectId: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onImport: (tools: z.infer<typeof WorkflowTool>[]) => void;
|
||||
}
|
||||
|
||||
export function McpImportTools({ projectId, isOpen, onOpenChange, onImport }: McpImportToolsProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tools, setTools] = useState<z.infer<typeof WorkflowTool>[]>([]);
|
||||
const [selectedTools, setSelectedTools] = useState<Set<number>>(new Set());
|
||||
|
||||
const process = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSelectedTools(new Set());
|
||||
try {
|
||||
const result = await fetchMcpTools(projectId);
|
||||
setTools(result);
|
||||
// Select all tools by default
|
||||
setSelectedTools(new Set(result.map((_, index) => index)));
|
||||
} catch (error) {
|
||||
setError(`Unable to fetch tools: ${error}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("mcp import tools useEffect", isOpen);
|
||||
if (isOpen) {
|
||||
process();
|
||||
}
|
||||
}, [isOpen, process]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="xl">
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>Import from MCP servers</ModalHeader>
|
||||
<ModalBody>
|
||||
{loading && <div className="flex gap-2 items-center">
|
||||
<Spinner size="sm" />
|
||||
Fetching tools...
|
||||
</div>}
|
||||
{error && <div className="bg-red-100 p-2 rounded-md text-red-800 flex items-center gap-2 text-sm">
|
||||
{error}
|
||||
<Button size="sm" color="danger" onPress={() => process()}>Retry</Button>
|
||||
</div>}
|
||||
{!loading && !error && <>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="text-gray-600">
|
||||
{tools.length === 0 ? "No tools found" : `Found ${tools.length} tools:`}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
setTools([]);
|
||||
process();
|
||||
}}
|
||||
startContent={<RefreshCwIcon className="w-4 h-4" />}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
{tools.length > 0 && <div className="flex flex-col w-full mt-4">
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-gray-50 rounded-t-lg border-b text-sm text-gray-700 font-medium">
|
||||
<div className="w-8">
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={selectedTools.size === tools.length}
|
||||
isIndeterminate={selectedTools.size > 0 && selectedTools.size < tools.length}
|
||||
onValueChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedTools(new Set(tools.map((_, i) => i)));
|
||||
} else {
|
||||
setSelectedTools(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-36">Server</div>
|
||||
<div className="flex-1">Tool Name</div>
|
||||
</div>
|
||||
<div className="border rounded-b-lg divide-y overflow-y-auto max-h-[300px]">
|
||||
{tools.map((t, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-4 px-4 py-2 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="w-8">
|
||||
<Checkbox
|
||||
size="sm"
|
||||
isSelected={selectedTools.has(index)}
|
||||
onValueChange={(checked) => {
|
||||
const newSelected = new Set(selectedTools);
|
||||
if (checked) {
|
||||
newSelected.add(index);
|
||||
} else {
|
||||
newSelected.delete(index);
|
||||
}
|
||||
setSelectedTools(newSelected);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-36">
|
||||
<div className="bg-blue-50 px-2 py-1 rounded text-blue-700 text-sm font-medium border border-blue-100">
|
||||
{t.mcpServerName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 truncate text-gray-700">{t.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
{tools.length > 0 && (
|
||||
<div className="mt-4 text-sm text-gray-600">
|
||||
{selectedTools.size} of {tools.length} tools selected
|
||||
</div>
|
||||
)}
|
||||
</>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="flat" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
{tools.length > 0 && <Button size="sm" onPress={() => {
|
||||
const selectedToolsList = tools.filter((_, index) => selectedTools.has(index));
|
||||
onImport(selectedToolsList);
|
||||
onClose();
|
||||
}}>
|
||||
Import
|
||||
</Button>}
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,16 +6,15 @@ import { ActionButton, StructuredPanel } from "../../../lib/components/structure
|
|||
import { EditableField } from "../../../lib/components/editable-field";
|
||||
import { Divider } from "@heroui/react";
|
||||
import { Label } from "../../../lib/components/label";
|
||||
import { TrashIcon, XIcon } from "lucide-react";
|
||||
import { ImportIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link as NextUILink } from "@heroui/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function ParameterConfig({
|
||||
param,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
handleRename
|
||||
handleRename,
|
||||
readOnly
|
||||
}: {
|
||||
param: {
|
||||
name: string,
|
||||
|
|
@ -29,11 +28,12 @@ export function ParameterConfig({
|
|||
required: boolean
|
||||
}) => void,
|
||||
handleDelete: (name: string) => void,
|
||||
handleRename: (oldName: string, newName: string) => void
|
||||
handleRename: (oldName: string, newName: string) => void,
|
||||
readOnly?: boolean
|
||||
}) {
|
||||
return <StructuredPanel
|
||||
title={param.name}
|
||||
actions={[
|
||||
actions={!readOnly ? [
|
||||
<ActionButton
|
||||
key="delete"
|
||||
onClick={() => handleDelete(param.name)}
|
||||
|
|
@ -41,7 +41,7 @@ export function ParameterConfig({
|
|||
>
|
||||
Remove
|
||||
</ActionButton>
|
||||
]}
|
||||
] : []}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<EditableField
|
||||
|
|
@ -52,6 +52,7 @@ export function ParameterConfig({
|
|||
handleRename(param.name, newName);
|
||||
}
|
||||
}}
|
||||
locked={readOnly}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
|
@ -69,6 +70,7 @@ export function ParameterConfig({
|
|||
type: Array.from(keys)[0] as string
|
||||
});
|
||||
}}
|
||||
isDisabled={readOnly}
|
||||
>
|
||||
{['string', 'number', 'boolean', 'array', 'object'].map(type => (
|
||||
<SelectItem key={type}>
|
||||
|
|
@ -89,6 +91,7 @@ export function ParameterConfig({
|
|||
description: desc
|
||||
});
|
||||
}}
|
||||
locked={readOnly}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
|
@ -102,6 +105,7 @@ export function ParameterConfig({
|
|||
required: !param.required
|
||||
});
|
||||
}}
|
||||
isDisabled={readOnly}
|
||||
>
|
||||
Required
|
||||
</Checkbox>
|
||||
|
|
@ -121,6 +125,7 @@ export function ToolConfig({
|
|||
handleClose: () => void
|
||||
}) {
|
||||
const [selectedParams, setSelectedParams] = useState(new Set([]));
|
||||
const isReadOnly = tool.isMcp;
|
||||
|
||||
function handleParamRename(oldName: string, newName: string) {
|
||||
const newProperties = { ...tool.parameters!.properties };
|
||||
|
|
@ -193,6 +198,13 @@ export function ToolConfig({
|
|||
</ActionButton>
|
||||
]}>
|
||||
<div className="flex flex-col gap-4">
|
||||
{tool.isMcp && <div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-normal bg-gray-100 px-2 py-1 rounded-md text-gray-700">
|
||||
<ImportIcon className="w-4 h-4 text-blue-700" />
|
||||
<div className="text-sm font-normal">Imported from MCP server: <span className="font-bold">{tool.mcpServerName}</span></div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<EditableField
|
||||
label="Name"
|
||||
value={tool.name}
|
||||
|
|
@ -209,6 +221,7 @@ export function ToolConfig({
|
|||
}
|
||||
return { valid: true };
|
||||
}}
|
||||
locked={isReadOnly}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
|
@ -221,89 +234,92 @@ export function ToolConfig({
|
|||
description: value
|
||||
})}
|
||||
placeholder="Describe what this tool does..."
|
||||
locked={isReadOnly}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Label label="TOOL RESPONSES" />
|
||||
{!isReadOnly && <>
|
||||
<Label label="TOOL RESPONSES" />
|
||||
|
||||
<div className="ml-4 flex flex-col gap-2">
|
||||
<RadioGroup
|
||||
defaultValue="mock"
|
||||
value={tool.mockTool ? "mock" : "api"}
|
||||
onValueChange={(value) => handleUpdate({
|
||||
...tool,
|
||||
mockTool: value === "mock",
|
||||
autoSubmitMockedResponse: value === "mock" ? true : undefined
|
||||
})}
|
||||
orientation="horizontal"
|
||||
classNames={{
|
||||
wrapper: "gap-8",
|
||||
label: "text-sm"
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
value="mock"
|
||||
size="sm"
|
||||
<div className="ml-4 flex flex-col gap-2">
|
||||
<RadioGroup
|
||||
defaultValue="mock"
|
||||
value={tool.mockTool ? "mock" : "api"}
|
||||
onValueChange={(value) => handleUpdate({
|
||||
...tool,
|
||||
mockTool: value === "mock",
|
||||
autoSubmitMockedResponse: value === "mock" ? true : undefined
|
||||
})}
|
||||
orientation="horizontal"
|
||||
classNames={{
|
||||
base: "max-w-[50%]",
|
||||
label: "text-sm font-normal"
|
||||
wrapper: "gap-8",
|
||||
label: "text-sm"
|
||||
}}
|
||||
>
|
||||
Mock tool responses
|
||||
</Radio>
|
||||
<Radio
|
||||
value="api"
|
||||
size="sm"
|
||||
classNames={{
|
||||
base: "max-w-[50%]",
|
||||
label: "text-sm font-normal"
|
||||
}}
|
||||
>
|
||||
Connect tool to your API
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
{tool.mockTool && <>
|
||||
<div className="ml-0">
|
||||
<Checkbox
|
||||
key="autoSubmitMockedResponse"
|
||||
<Radio
|
||||
value="mock"
|
||||
size="sm"
|
||||
classNames={{
|
||||
label: "text-xs font-normal"
|
||||
base: "max-w-[50%]",
|
||||
label: "text-sm font-normal"
|
||||
}}
|
||||
isSelected={tool.autoSubmitMockedResponse ?? true}
|
||||
onValueChange={(value) => handleUpdate({
|
||||
...tool,
|
||||
autoSubmitMockedResponse: value
|
||||
})}
|
||||
>
|
||||
Auto-submit mocked response in playground
|
||||
</Checkbox>
|
||||
</div>
|
||||
Mock tool responses
|
||||
</Radio>
|
||||
<Radio
|
||||
value="api"
|
||||
size="sm"
|
||||
classNames={{
|
||||
base: "max-w-[50%]",
|
||||
label: "text-sm font-normal"
|
||||
}}
|
||||
>
|
||||
Connect tool to your API
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<Divider />
|
||||
{tool.mockTool && <>
|
||||
<div className="ml-0">
|
||||
<Checkbox
|
||||
key="autoSubmitMockedResponse"
|
||||
size="sm"
|
||||
classNames={{
|
||||
label: "text-xs font-normal"
|
||||
}}
|
||||
isSelected={tool.autoSubmitMockedResponse ?? true}
|
||||
onValueChange={(value) => handleUpdate({
|
||||
...tool,
|
||||
autoSubmitMockedResponse: value
|
||||
})}
|
||||
>
|
||||
Auto-submit mocked response in playground
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<EditableField
|
||||
label="Mock instructions"
|
||||
value={tool.mockInstructions || ''}
|
||||
onChange={(value) => handleUpdate({
|
||||
...tool,
|
||||
mockInstructions: value
|
||||
})}
|
||||
placeholder="Enter mock instructions..."
|
||||
multiline
|
||||
/>
|
||||
</>}
|
||||
<Divider />
|
||||
|
||||
{!tool.mockTool && (
|
||||
<div className="ml-0 text-danger text-xs">
|
||||
Please configure your webhook in the <strong>Integrate</strong> page if you haven't already.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<EditableField
|
||||
label="Mock instructions"
|
||||
value={tool.mockInstructions || ''}
|
||||
onChange={(value) => handleUpdate({
|
||||
...tool,
|
||||
mockInstructions: value
|
||||
})}
|
||||
placeholder="Enter mock instructions..."
|
||||
multiline
|
||||
/>
|
||||
</>}
|
||||
|
||||
<Divider />
|
||||
{!tool.mockTool && (
|
||||
<div className="ml-0 text-danger text-xs">
|
||||
Please configure your webhook in the <strong>Integrate</strong> page if you haven't already.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
</>}
|
||||
|
||||
<Label label="Parameters" />
|
||||
|
||||
|
|
@ -320,11 +336,12 @@ export function ToolConfig({
|
|||
handleUpdate={handleParamUpdate}
|
||||
handleDelete={handleParamDelete}
|
||||
handleRename={handleParamRename}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
{!isReadOnly && <Button
|
||||
className="self-start shrink-0"
|
||||
variant="light"
|
||||
size="sm"
|
||||
|
|
@ -352,7 +369,7 @@ export function ToolConfig({
|
|||
}}
|
||||
>
|
||||
Add Parameter
|
||||
</Button>
|
||||
</Button>}
|
||||
</div>
|
||||
</StructuredPanel>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ import { apiV1 } from "rowboat-shared";
|
|||
import { publishWorkflow, renameWorkflow, saveWorkflow } from "../../../actions/workflow_actions";
|
||||
import { PublishedBadge } from "./published_badge";
|
||||
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
|
||||
import { CopyIcon, Layers2Icon, RadioIcon, RedoIcon, Sparkles, UndoIcon } from "lucide-react";
|
||||
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon } from "lucide-react";
|
||||
import { EntityList } from "./entity_list";
|
||||
import { CopilotMessage } from "../../../lib/types/copilot_types";
|
||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||
import { McpImportTools } from "./mcp_imports";
|
||||
|
||||
enablePatches();
|
||||
|
||||
|
|
@ -132,6 +132,9 @@ export type Action = {
|
|||
} | {
|
||||
type: "restore_state";
|
||||
state: StateItem;
|
||||
} | {
|
||||
type: "import_mcp_tools";
|
||||
tools: z.infer<typeof WorkflowTool>[];
|
||||
};
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
|
|
@ -509,6 +512,26 @@ function reducer(state: State, action: Action): State {
|
|||
draft.workflow.startAgent = action.name;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
case "import_mcp_tools":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
// Process each tool one by one
|
||||
action.tools.forEach(newTool => {
|
||||
const existingToolIndex = draft.workflow.tools.findIndex(
|
||||
tool => tool.name === newTool.name
|
||||
);
|
||||
|
||||
if (existingToolIndex !== -1) {
|
||||
// Replace existing tool
|
||||
draft.workflow.tools[existingToolIndex] = newTool;
|
||||
} else {
|
||||
// Add new tool
|
||||
draft.workflow.tools.push(newTool);
|
||||
}
|
||||
});
|
||||
draft.chatKey++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -575,6 +598,7 @@ export function WorkflowEditor({
|
|||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
const [loadingMessage, setLoadingMessage] = useState("Thinking...");
|
||||
const [responseError, setResponseError] = useState<string | null>(null);
|
||||
const [isMcpImportModalOpen, setIsMcpImportModalOpen] = useState(false);
|
||||
|
||||
console.log(`workflow editor chat key: ${state.present.chatKey}`);
|
||||
|
||||
|
|
@ -697,6 +721,10 @@ export function WorkflowEditor({
|
|||
}
|
||||
}, [isLive]);
|
||||
|
||||
function handleImportMcpTools(tools: z.infer<typeof WorkflowTool>[]) {
|
||||
dispatch({ type: "import_mcp_tools", tools });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (state.present.pendingChanges && state.present.workflow) {
|
||||
saveQueue.current.push(state.present.workflow);
|
||||
|
|
@ -732,7 +760,7 @@ export function WorkflowEditor({
|
|||
<DropdownMenu
|
||||
disabledKeys={[
|
||||
...(state.present.pendingChanges ? ['switch', 'clone'] : []),
|
||||
...(isLive ? ['publish'] : []),
|
||||
...(isLive ? ['publish', 'mcp'] : []),
|
||||
]}
|
||||
onAction={(key) => {
|
||||
if (key === 'switch') {
|
||||
|
|
@ -747,6 +775,9 @@ export function WorkflowEditor({
|
|||
if (key === 'clipboard') {
|
||||
handleCopyJSON();
|
||||
}
|
||||
if (key === 'mcp') {
|
||||
setIsMcpImportModalOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
|
|
@ -774,6 +805,12 @@ export function WorkflowEditor({
|
|||
>
|
||||
Copy as JSON
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="mcp"
|
||||
startContent={<ImportIcon className="w-4 h-4 text-blue-700" />}
|
||||
>
|
||||
MCP: Import tools
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
@ -935,5 +972,11 @@ export function WorkflowEditor({
|
|||
</ResizablePanel>
|
||||
</>}
|
||||
</ResizablePanelGroup>
|
||||
<McpImportTools
|
||||
projectId={state.present.workflow.projectId}
|
||||
isOpen={isMcpImportModalOpen}
|
||||
onOpenChange={setIsMcpImportModalOpen}
|
||||
onImport={handleImportMcpTools}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
|||
858
apps/rowboat/package-lock.json
generated
858
apps/rowboat/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -19,12 +19,13 @@
|
|||
"@aws-sdk/s3-request-presigner": "^3.743.0",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@langchain/core": "^0.3.7",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@mendable/firecrawl-js": "^1.0.3",
|
||||
"@heroui/react": "2.7.4",
|
||||
"@heroui/system": "2.4.11",
|
||||
"@heroui/theme": "2.4.11",
|
||||
"@langchain/core": "^0.3.7",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@mendable/firecrawl-js": "^1.0.3",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@primer/react": "^36.27.0",
|
||||
"@qdrant/js-client-rest": "^1.13.0",
|
||||
"ai": "^3.3.28",
|
||||
|
|
@ -72,4 +73,4 @@
|
|||
"tsx": "^4.19.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue