Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '@/app/workspace/[workspaceId]/templates/components/template-card'
import { getKeyboardShortcutText } from '@/app/workspace/[workspaceId]/w/hooks/use-keyboard-shortcuts'
import { getAllBlocks } from '@/blocks'
import { McpTools } from '@/components/mcp-tools'
import { type NavigationSection, useSearchNavigation } from './hooks/use-search-navigation'

interface SearchModalProps {
Expand Down Expand Up @@ -654,6 +655,9 @@ export function SearchModal({
</div>
)}

{/* MCP Tools Section */}
{isOnWorkflowPage && <McpTools onToolClick={handleBlockClick} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: MCP Tools section is not included in keyboard navigation system - users won't be able to navigate to these tools using arrow keys


{/* Tools Section */}
{filteredTools.length > 0 && (
<div>
Expand Down
112 changes: 112 additions & 0 deletions apps/sim/components/mcp-tools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use client'

import { useEffect, useState } from 'react'
import { getMcpTools } from '@/lib/mcp'

interface McpTool {
id: string
name: string
description: string
icon: React.ComponentType<any>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Using React.ComponentType<any> violates type safety. Consider defining a more specific icon type or using a union of expected icon prop types.

Context Used: Context - Avoid using type assertions to 'any' in TypeScript. Instead, ensure proper type definitions are used to maintain type safety. (link)

bgColor: string
type: string
server: string
}

interface McpToolsProps {
onToolClick: (toolType: string) => void
}

export function McpTools({ onToolClick }: McpToolsProps) {
const [mcpTools, setMcpTools] = useState<McpTool[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

async function fetchMcpTools() {
setIsLoading(true)
setError(null)
try {
const tools = await getMcpTools()
setMcpTools(tools)
} catch (error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The error variable shadows the catch parameter. Consider renaming one of them to avoid confusion.

Suggested change
} catch (error) {
} catch (err) {

console.error('Error fetching MCP tools:', error)
setError('Failed to load MCP tools. Please ensure the MCPO server is running and the configuration is correct.')
} finally {
setIsLoading(false)
}
}

useEffect(() => {
fetchMcpTools()
}, [])

const groupedTools = mcpTools.reduce(
(acc, tool) => {
if (!acc[tool.server]) {
acc[tool.server] = []
}
acc[tool.server].push(tool)
return acc
},
{} as Record<string, McpTool[]>
)

if (isLoading) {
return <div>Loading MCP Tools...</div>
}

if (error) {
return (
<div className="text-red-500 p-4">
<p>{error}</p>
<button
onClick={fetchMcpTools}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Retry
</button>
</div>
)
}

return (
<div>
{Object.entries(groupedTools).map(([server, tools]) => (
<div key={server}>
<h3 className='mb-3 ml-6 font-normal font-sans text-muted-foreground text-sm leading-none tracking-normal'>
{server}
</h3>
<div
className='scrollbar-none flex gap-2 overflow-x-auto px-6 pb-1'
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{tools.map((tool) => (
<button
key={tool.id}
onClick={() => onToolClick(tool.type)}
className='flex h-auto w-[180px] flex-shrink-0 cursor-pointer flex-col items-start gap-2 rounded-[8px] border p-3 transition-all duration-200 border-border/40 bg-background/60 hover:border-border hover:bg-secondary/80'
>
<div className='flex items-center gap-2'>
<div
className='flex h-5 w-5 items-center justify-center rounded-[4px]'
style={{ backgroundColor: tool.bgColor }}
>
<tool.icon className='!h-3.5 !w-3.5 text-white' />
</div>
<span className='font-medium font-sans text-foreground text-sm leading-none tracking-normal'>
{tool.name}
</span>
</div>
{tool.description && (
<p className='line-clamp-2 text-left text-muted-foreground text-xs'>
{tool.description}
</p>
)}
</button>
))}
</div>
</div>
))}
</div>
)
}
92 changes: 92 additions & 0 deletions apps/sim/lib/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { promises as fs } from 'fs';
import path from 'path';
import { z } from 'zod';
import { ApiIcon } from '@/components/icons';

const McpToolSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
icon: z.any(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Icon field uses 'any' type. Consider using proper React component type like React.ComponentType<any>.

Context Used: Context - Avoid using type assertions to 'any' in TypeScript. Instead, ensure proper type definitions are used to maintain type safety. (link)

bgColor: z.string(),
type: z.string(),
server: z.string(),
});

export type McpTool = z.infer<typeof McpToolSchema>;

const McpoConfigSchema = z.object({
mcpServers: z.record(z.object({
command: z.string(),
args: z.array(z.string()),
url: z.string().optional(),
})),
});

async function readMcpoConfig() {
const configPath = path.resolve(process.cwd(), 'mcpo-config.json');
try {
const fileContent = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(fileContent);
return McpoConfigSchema.parse(config);
} catch (error) {
console.error('Error reading or parsing mcpo-config.json:', error);
throw new Error('Could not read or parse mcpo-config.json');
}
}

export async function getMcpTools(): Promise<McpTool[]> {
const config = await readMcpoConfig();
const allTools: McpTool[] = [];

for (const serverName in config.mcpServers) {
const serverConfig = config.mcpServers[serverName];
// For now, let's assume a default URL if not provided.
// This will be improved in the dynamic configuration step.
const openapi_url = serverConfig.url || `http://localhost:8000/openapi.json`;

try {
const response = await fetch(openapi_url);
if (!response.ok) {
console.error(`Error fetching OpenAPI schema from ${serverName}: ${response.statusText}`);
continue;
}
const openapi_spec = await response.json();

const serverTools = transformOpenAPIToMcpTools(openapi_spec, serverName);
allTools.push(...serverTools);

} catch (error) {
console.error(`Error fetching or parsing OpenAPI schema from ${serverName}:`, error);
}
}

return allTools;
}

function transformOpenAPIToMcpTools(openapi_spec: any, serverName: string): McpTool[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Type assertion to 'any' breaks type safety. Define proper OpenAPI spec interface.

Context Used: Context - Avoid using type assertions to 'any' in TypeScript. Instead, ensure proper type definitions are used to maintain type safety. (link)

const tools: McpTool[] = [];
if (!openapi_spec.paths) {
return tools;
}

for (const path in openapi_spec.paths) {
const pathItem = openapi_spec.paths[path];
for (const method in pathItem) {
const operation = pathItem[method];
if (operation.operationId) {
tools.push({
id: operation.operationId,
name: operation.summary || operation.operationId,
description: operation.description || '',
icon: ApiIcon,
bgColor: '#6B7280', // Default color
type: operation.operationId,
server: serverName,
});
}
}
}

return tools;
}
13 changes: 13 additions & 0 deletions apps/sim/lib/mcp/tool-servers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// This file defines the MCP tool servers that will be available in the application.

interface McpToolServer {
name: string
command: string[]
}

export const mcpToolServers: McpToolServer[] = [
{
name: 'Time Server',
command: ['uvx', 'mcp-server-time', '--local-timezone=America/New_York'],
},
]
Comment on lines +8 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Configuration structure mismatch: This uses command: string[] but existing MCP config expects separate command: string and args: string[] fields. This inconsistency could cause integration issues.

2 changes: 2 additions & 0 deletions apps/sim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"node": ">=20.0.0"
},
"scripts": {
"predev": "bun run scripts/generate-mcpo-config.ts",
"prestart": "bun run scripts/generate-mcpo-config.ts",
"dev": "next dev --turbo --port 3000",
"dev:classic": "next dev",
"dev:sockets": "bun run socket-server/index.ts",
Expand Down
56 changes: 56 additions & 0 deletions apps/sim/scripts/generate-mcpo-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { promises as fs } from 'fs';
import path from 'path';

interface ToolServer {
name: string;
command: string;
args: string[];
url: string;
}

interface ToolServersConfig {
servers: ToolServer[];
}

interface McpoServerConfig {
[key: string]: {
command: string;
args: string[];
url: string;
};
}
Comment on lines 15 to 21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Interface structure doesn't match existing McpoConfigSchema in lib/mcp/index.ts - the url property should be optional


interface McpoConfig {
mcpServers: McpoServerConfig;
}

async function generateMcpoConfig() {
try {
const toolServersPath = path.resolve(process.cwd(), 'tool-servers.json');
const toolServersContent = await fs.readFile(toolServersPath, 'utf-8');
const toolServersConfig: ToolServersConfig = JSON.parse(toolServersContent);

const mcpServers: McpoServerConfig = {};
for (const server of toolServersConfig.servers) {
mcpServers[server.name] = {
command: server.command,
args: server.args,
url: server.url,
};
}

const mcpoConfig: McpoConfig = {
mcpServers,
};

const mcpoConfigPath = path.resolve(process.cwd(), 'mcpo-config.json');
await fs.writeFile(mcpoConfigPath, JSON.stringify(mcpoConfig, null, 2));

console.log('Successfully generated mcpo-config.json');
} catch (error) {
console.error('Error generating mcpo-config.json:', error);
process.exit(1);
}
}

generateMcpoConfig();
8 changes: 8 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,13 @@ services:
timeout: 5s
retries: 5

mcpo:
image: openwebui/mcpo:latest
ports:
- '8000:8000'
volumes:
- ./mcpo-config.json:/app/config/config.json
command: ['uvx', 'mcpo', '--host', '0.0.0.0', '--port', '8000']
Comment on lines +94 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Missing health check for the mcpo service. Consider adding a health check similar to other services to ensure proper startup ordering.

Comment on lines +94 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: No restart policy specified for mcpo service. Consider adding restart: unless-stopped for consistency with other services.


volumes:
postgres_data:
8 changes: 8 additions & 0 deletions mcpo-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"time": {
"command": "uvx",
"args": ["mcp-server-time", "--local-timezone=America/New_York"]
}
Comment on lines +3 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing url field that the MCP tool discovery system expects. The getMcpTools() function will fall back to http://localhost:8000/openapi.json which may not be correct for this server.

}
}
10 changes: 10 additions & 0 deletions tool-servers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"servers": [
{
"name": "time",
"command": "uvx",
"args": ["mcp-server-time", "--local-timezone=America/New_York"],
"url": "http://localhost:8001/openapi.json"
}
]
}