diff --git a/app/actions.tsx b/app/actions.tsx index eeb10cfb..64354705 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -12,6 +12,7 @@ import { Spinner } from '@/components/ui/spinner'; import { Section } from '@/components/section'; import { FollowupPanel } from '@/components/followup-panel'; import { inquire, researcher, taskManager, querySuggestor } from '@/lib/agents'; +import { useGeospatialToolMcp } from '@/lib/agents/tools/geospatial'; // Added import for the hook import { writer } from '@/lib/agents/writer'; import { saveChat, getSystemPrompt } from '@/lib/actions/chat'; // Added getSystemPrompt import { Chat, AIMessage } from '@/lib/types'; @@ -29,7 +30,7 @@ type RelatedQueries = { items: { query: string }[]; }; -async function submit(formData?: FormData, skip?: boolean) { +async function submit(formData?: FormData, skip?: boolean, mcp?: any) { // Added mcp as a parameter 'use server'; // TODO: Update agent function signatures in lib/agents/researcher.tsx and lib/agents/writer.tsx @@ -145,6 +146,7 @@ async function submit(formData?: FormData, skip?: boolean) { uiStream, streamText, messages, + mcp, // Pass mcp to researcher useSpecificAPI ); answer = fullResponse; diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index fbf53bd6..f29a8a6f 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useRef } from 'react' import { useRouter } from 'next/navigation' import type { AI, UIState } from '@/app/actions' import { useUIState, useActions } from 'ai/rsc' +import { useGeospatialToolMcp } from '@/lib/agents/tools/geospatial' // Added import import { cn } from '@/lib/utils' import { UserMessage } from './user-message' import { Button } from './ui/button' @@ -20,6 +21,7 @@ interface ChatPanelProps { export function ChatPanel({ messages, input, setInput }: ChatPanelProps) { const [, setMessages] = useUIState() const { submit } = useActions() + const mcp = useGeospatialToolMcp() // Call the hook const [isButtonPressed, setIsButtonPressed] = useState(false) const [isMobile, setIsMobile] = useState(false) const inputRef = useRef(null) @@ -56,7 +58,7 @@ export function ChatPanel({ messages, input, setInput }: ChatPanelProps) { } ]) const formData = new FormData(e.currentTarget) - const responseMessage = await submit(formData) + const responseMessage = await submit(formData, undefined, mcp) // Pass mcp setMessages(currentMessages => [...currentMessages, responseMessage as any]) } diff --git a/components/copilot.tsx b/components/copilot.tsx index 57bee561..f4ddd0a5 100644 --- a/components/copilot.tsx +++ b/components/copilot.tsx @@ -8,9 +8,9 @@ import { Button } from './ui/button' import { Card } from './ui/card' import { ArrowRight, Check, FastForward, Sparkles } from 'lucide-react' import { useActions, useStreamableValue, useUIState } from 'ai/rsc' +import { useGeospatialToolMcp } from '@/lib/agents/tools/geospatial' // Added import import type { AI } from '@/app/actions' -import { - +import { } from './ui/icons' @@ -32,6 +32,7 @@ export const Copilot: React.FC = ({ inquiry }: CopilotProps) => { const [isButtonDisabled, setIsButtonDisabled] = useState(true) const [, setMessages] = useUIState() const { submit } = useActions() + const mcp = useGeospatialToolMcp() // Call the hook const handleInputChange = (event: React.ChangeEvent) => { setQuery(event.target.value) @@ -78,7 +79,7 @@ export const Copilot: React.FC = ({ inquiry }: CopilotProps) => { ? undefined : new FormData(e.target as HTMLFormElement) - const response = await submit(formData, skip) + const response = await submit(formData, skip, mcp) // Pass mcp setMessages(currentMessages => [...currentMessages, response]) } diff --git a/components/followup-panel.tsx b/components/followup-panel.tsx index 0cdc63c7..9297e9fb 100644 --- a/components/followup-panel.tsx +++ b/components/followup-panel.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { Button } from './ui/button' import { Input } from './ui/input' import { useActions, useUIState } from 'ai/rsc' +import { useGeospatialToolMcp } from '@/lib/agents/tools/geospatial' // Added import import type { AI } from '@/app/actions' import { UserMessage } from './user-message' import { ArrowRight } from 'lucide-react' @@ -11,6 +12,7 @@ import { ArrowRight } from 'lucide-react' export function FollowupPanel() { const [input, setInput] = useState('') const { submit } = useActions() + const mcp = useGeospatialToolMcp() // Call the hook const [, setMessages] = useUIState() const handleSubmit = async (event: React.FormEvent) => { @@ -23,7 +25,7 @@ export function FollowupPanel() { component: } - const responseMessage = await submit(formData) + const responseMessage = await submit(formData, undefined, mcp) // Pass mcp setMessages(currentMessages => [ ...currentMessages, userMessage, diff --git a/components/search-related.tsx b/components/search-related.tsx index eea23ab3..92a2ee68 100644 --- a/components/search-related.tsx +++ b/components/search-related.tsx @@ -11,6 +11,7 @@ import { StreamableValue } from 'ai/rsc' import { AI } from '@/app/actions' +import { useGeospatialToolMcp } from '@/lib/agents/tools/geospatial' // Added import import { UserMessage } from './user-message' import { PartialRelated } from '@/lib/schema/related' @@ -22,6 +23,7 @@ export const SearchRelated: React.FC = ({ relatedQueries }) => { const { submit } = useActions() + const mcp = useGeospatialToolMcp() // Call the hook const [, setMessages] = useUIState() const [data, error, pending] = useStreamableValue(relatedQueries) @@ -44,7 +46,7 @@ export const SearchRelated: React.FC = ({ component: } - const responseMessage = await submit(formData) + const responseMessage = await submit(formData, undefined, mcp) // Pass mcp setMessages(currentMessages => [ ...currentMessages, userMessage, diff --git a/lib/agents/researcher.tsx b/lib/agents/researcher.tsx index 01a0aa1f..28dfc11f 100644 --- a/lib/agents/researcher.tsx +++ b/lib/agents/researcher.tsx @@ -16,6 +16,7 @@ export async function researcher( uiStream: ReturnType, streamText: ReturnType>, messages: CoreMessage[], + mcp: any, // Moved mcp before optional parameter useSpecificModel?: boolean ) { let fullResponse = '' @@ -54,7 +55,8 @@ Match the language of your response to the user's language.`; messages, tools: getTools({ uiStream, - fullResponse + fullResponse, + mcp // Pass the mcp parameter to getTools }) }) diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index 15261d63..967d54dd 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -1,21 +1,18 @@ -import { createStreamableValue } from 'ai/rsc'; +import dotenv from 'dotenv'; +dotenv.config(); + import { useMcp } from 'use-mcp/react'; +import { createStreamableUI, createStreamableValue } from 'ai/rsc'; + import { BotMessage } from '@/components/message'; import { geospatialQuerySchema } from '@/lib/schema/geospatial'; -import { ToolProps } from '.'; -// Ensure dotenv is configured early in the application (if not already done in a parent module) -// This is typically done in the entry point (e.g., index.ts or app.ts), but included here for clarity -import dotenv from 'dotenv'; -dotenv.config(); +// Define proper MCP type +export type McpClient = ReturnType; -// Move the MCP logic into a custom hook export function useGeospatialToolMcp() { const mcpServerUrl = 'https://server.smithery.ai/mapbox-mcp-server/mcp?api_key=705b0222-a657-4cd2-b180-80c406cf6179&profile=smooth-lemur-vfUbUE'; - // Alternative: Dynamic URL (uncomment if using environment variables) - // const mcpServerUrl = `https://server.smithery.ai/@ngoiyaeric/mapbox-mcp-server/mcp?profile=${process.env.SMITHERY_PROFILE_ID}&api_key=${process.env.SMITHERY_API_KEY}`; - const mcp = useMcp({ url: mcpServerUrl, debug: process.env.NODE_ENV === 'development', @@ -26,7 +23,13 @@ export function useGeospatialToolMcp() { return mcp; } -export const geospatialTool = ({ uiStream, mcp }: ToolProps & { mcp: ReturnType }) => ({ +export const geospatialTool = ({ + uiStream, + mcp, +}: { + uiStream: ReturnType; + mcp: McpClient | null; +}) => ({ description: `Use this tool for any queries that involve locations, places, addresses, distances between places, directions, or finding points of interest on a map. This includes questions like: - 'Where is [place name/address]?' - 'Show me [place name/address] on the map.' @@ -43,8 +46,25 @@ export const geospatialTool = ({ uiStream, mcp }: ToolProps & { mcp: ReturnType< uiFeedbackStream.done(`Looking up map information for: "${query}"...`); uiStream.append(); + // Check if MCP client is available + if (!mcp) { + console.warn('MCP client is not available, cannot proceed with geospatial query'); + const errorStream = createStreamableValue(); + errorStream.done('Geospatial functionality is currently unavailable.'); + uiStream.append(); + return { + type: 'MAP_QUERY_TRIGGER', + originalUserInput: query, + timestamp: new Date().toISOString(), + mcp_response: null, + error: 'MCP client not available', + }; + } + // Log environment variables for debugging (with API key masked) - console.log(`[GeospatialTool] SMITHERY_PROFILE_ID: "${process.env.SMITHERY_PROFILE_ID ?? 'undefined'}"`); + console.log( + `[GeospatialTool] SMITHERY_PROFILE_ID: "${process.env.SMITHERY_PROFILE_ID ?? 'undefined'}"` + ); console.log( `[GeospatialTool] SMITHERY_API_KEY: ${ process.env.SMITHERY_API_KEY @@ -53,33 +73,54 @@ export const geospatialTool = ({ uiStream, mcp }: ToolProps & { mcp: ReturnType< }` ); - let mcpData: { - location: { - latitude?: number; - longitude?: number; - place_name?: string; - address?: string; - }; - mapUrl?: string; - } | null = null; + let mcpData: + | { + location: { + latitude?: number; + longitude?: number; + place_name?: string; + address?: string; + }; + mapUrl?: string; + } + | null = null; try { console.log(`Attempting to connect to MCP server...`); - if (mcp.state !== 'ready') { - console.warn(`MCP client not ready (state: ${mcp.state}), cannot proceed with tool call.`); - throw new Error(`MCP client not ready (state: ${mcp.state})`); + if (!mcp || mcp.state !== 'ready') { + console.warn(`MCP client not ready${mcp ? ` (state: ${mcp.state})` : ''}, cannot proceed with tool call.`); + const errorStream = createStreamableValue(); + errorStream.done(`MCP client not ready${mcp ? ` (state: ${mcp.state})` : ''}. Please try again.`); + uiStream.append(); + return { + type: 'MAP_QUERY_TRIGGER', + originalUserInput: query, + timestamp: new Date().toISOString(), + mcp_response: null, + error: `MCP client not ready${mcp ? ` (state: ${mcp.state})` : ''}`, + }; } console.log('✅ Successfully connected to MCP server.'); const geocodeParams = { query, includeMapPreview: true }; - console.log('📞 Attempting to call "geocode_location" tool with params:', geocodeParams); - const geocodeResult = await mcp.callTool('geocode_location', geocodeParams); + console.log( + '📞 Attempting to call "geocode_location" tool with params:', + geocodeParams + ); + const geocodeResult = await mcp.callTool( + 'geocode_location', + geocodeParams + ); if (geocodeResult?.content && Array.isArray(geocodeResult.content)) { - const lastContentItem = geocodeResult.content[geocodeResult.content.length - 1]; - if (lastContentItem?.type === 'text' && typeof lastContentItem.text === 'string') { + const lastContentItem = + geocodeResult.content[geocodeResult.content.length - 1]; + if ( + lastContentItem?.type === 'text' && + typeof lastContentItem.text === 'string' + ) { const jsonRegex = /```json\n([\s\S]*?)\n```/; const match = lastContentItem.text.match(jsonRegex); if (match?.[1]) { @@ -97,7 +138,9 @@ export const geospatialTool = ({ uiStream, mcp }: ToolProps & { mcp: ReturnType< }; console.log('✅ Successfully parsed MCP geocode data:', mcpData); } else { - console.warn("⚠️ Parsed JSON from MCP does not contain expected 'location' field."); + console.warn( + "⚠️ Parsed JSON from MCP does not contain expected 'location' field." + ); } } catch (parseError) { console.error( @@ -108,10 +151,14 @@ export const geospatialTool = ({ uiStream, mcp }: ToolProps & { mcp: ReturnType< ); } } else { - console.warn('⚠️ Could not find JSON block in the expected format in MCP response.'); + console.warn( + '⚠️ Could not find JSON block in the expected format in MCP response.' + ); } } else { - console.warn('⚠️ Last content item from MCP is not a text block or is missing.'); + console.warn( + '⚠️ Last content item from MCP is not a text block or is missing.' + ); } } else { console.warn( @@ -122,8 +169,10 @@ export const geospatialTool = ({ uiStream, mcp }: ToolProps & { mcp: ReturnType< } catch (error) { console.error('❌ MCP connection or tool call failed:', error); } finally { - if (mcp.state === 'ready') { - console.log('\nMCP client is ready; no explicit close method available for useMcp.'); + if (mcp && mcp.state === 'ready') { + console.log( + '\n[GeospatialTool] MCP client is ready; no explicit close method available for useMcp.' + ); } } diff --git a/lib/agents/tools/index.tsx b/lib/agents/tools/index.tsx index e1c29170..cd3948a1 100644 --- a/lib/agents/tools/index.tsx +++ b/lib/agents/tools/index.tsx @@ -2,14 +2,15 @@ import { createStreamableUI } from 'ai/rsc' import { retrieveTool } from './retrieve' import { searchTool } from './search' import { videoSearchTool } from './video-search' -import { geospatialTool, useGeospatialToolMcp } from './geospatial'; // Added import +import { geospatialTool, useGeospatialToolMcp } from './geospatial' export interface ToolProps { uiStream: ReturnType fullResponse: string + mcp?: ReturnType } -export const getTools = ({ uiStream, fullResponse }: ToolProps) => { +export const getTools = ({ uiStream, fullResponse, mcp }: ToolProps) => { const tools: any = { search: searchTool({ uiStream, @@ -18,13 +19,11 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => { retrieve: retrieveTool({ uiStream, fullResponse - }), - - geospatialQueryTool: geospatialTool({ - uiStream, - fullResponse, - mcp: useGeospatialToolMcp() - }) + }), + geospatialQueryTool: geospatialTool({ + uiStream, + mcp: mcp || null + }) } if (process.env.SERPER_API_KEY) { @@ -35,20 +34,4 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => { } return tools -} - -export const useTools = ({ uiStream, fullResponse }: ToolProps) => { - const mcp = useGeospatialToolMcp(); - - const tools: any = { - search: searchTool({ uiStream, fullResponse }), - retrieve: retrieveTool({ uiStream, fullResponse }), - geospatialQueryTool: geospatialTool({ uiStream, fullResponse, mcp }), - }; - - if (process.env.SERPER_API_KEY) { - tools.videoSearch = videoSearchTool({ uiStream, fullResponse }); - } - - return tools; -}; +} \ No newline at end of file