diff --git a/bun.lockb b/bun.lockb index 2aff2ca1..daae4dfe 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index 80a05738..4ae4ffc6 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -33,7 +33,7 @@ async function getConnectedMcpClient(): Promise { let config; try { - const mapboxMcpConfig = await import('QCX/mapbox_mcp_config.json'); + const mapboxMcpConfig = await import('../../../mapbox_mcp_config.json'); config = { ...mapboxMcpConfig.default || mapboxMcpConfig, mapboxAccessToken diff --git a/lib/agents/tools/index.tsx b/lib/agents/tools/index.tsx index 4c08f373..919d2d1b 100644 --- a/lib/agents/tools/index.tsx +++ b/lib/agents/tools/index.tsx @@ -2,15 +2,18 @@ import { createStreamableUI } from 'ai/rsc' import { retrieveTool } from './retrieve' import { searchTool } from './search' import { videoSearchTool } from './video-search' -import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import +import { geospatialTool } from './geospatial' +import { mapboxGeocodingTool } from './mapbox/geocoding' +import { mapboxDirectionsTool } from './mapbox/directions' +import { mapboxMatrixTool } from './mapbox/matrix' +import { mapboxIsochroneTool } from './mapbox/isochrone' +import { mapboxStaticImageTool } from './mapbox/static-image' export interface ToolProps { uiStream: ReturnType fullResponse: string - // mcp?: any; // Removed mcp property as it's no longer passed down for geospatialTool } -// Removed mcp from parameters export const getTools = ({ uiStream, fullResponse }: ToolProps) => { const tools: any = { search: searchTool({ @@ -21,10 +24,28 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => { uiStream, fullResponse }), - // geospatialTool now only requires uiStream geospatialQueryTool: geospatialTool({ uiStream - // mcp: mcp || null // Removed mcp argument + }), + mapboxGeocoding: mapboxGeocodingTool({ + uiStream, + fullResponse + }), + mapboxDirections: mapboxDirectionsTool({ + uiStream, + fullResponse + }), + mapboxMatrix: mapboxMatrixTool({ + uiStream, + fullResponse + }), + mapboxIsochrone: mapboxIsochroneTool({ + uiStream, + fullResponse + }), + mapboxStaticImage: mapboxStaticImageTool({ + uiStream, + fullResponse }) } diff --git a/lib/agents/tools/mapbox/directions.tsx b/lib/agents/tools/mapbox/directions.tsx new file mode 100644 index 00000000..e8ecc8f9 --- /dev/null +++ b/lib/agents/tools/mapbox/directions.tsx @@ -0,0 +1,77 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { BotMessage } from '@/components/message' +import { Card } from '@/components/ui/card' +import { directionsSchema } from '@/lib/schema/mapbox' +import { getConnectedMcpClient, closeClient } from './mcp-client' +import { ToolProps } from '..' + +export const mapboxDirectionsTool = ({ uiStream }: ToolProps) => ({ + description: 'Get directions between two locations.', + parameters: directionsSchema, + execute: async ({ + origin, + destination, + profile + }: { + origin: string + destination: string + profile: 'driving' | 'walking' | 'cycling' + }) => { + const uiFeedbackStream = createStreamableValue() + uiStream.append() + + uiFeedbackStream.update( + `Getting ${profile} directions from "${origin}" to "${destination}"...` + ) + + const mcpClient = await getConnectedMcpClient() + if (!mcpClient) { + const error = + 'Mapbox tool is not available. Please check your configuration.' + uiFeedbackStream.update(error) + return { error } + } + + let result + try { + const toolArgs = { origin, destination, profile } + const directionsResult = await mcpClient.callTool({ + name: 'mapbox_directions', + arguments: toolArgs + }) + + const toolResults = (directionsResult as any)?.tool_results || [] + if (toolResults.length === 0 || !toolResults[0]?.content) { + throw new Error('No content returned from mapping service') + } + + let content = toolResults[0].content + if (typeof content === 'string') { + const jsonRegex = /```(?:json)?\n?([\s\S]*?)\n?```/ + const match = content.match(jsonRegex) + if (match) { + content = JSON.parse(match[1].trim()) + } else { + content = JSON.parse(content) + } + } + result = content + } catch (error: any) { + console.error('Mapbox directions tool error:', error) + const errorMessage = `Error getting directions: ${error.message}` + uiFeedbackStream.update(errorMessage) + result = { error: errorMessage } + } finally { + await closeClient(mcpClient) + } + + uiFeedbackStream.done() + uiStream.update( + +
{JSON.stringify(result, null, 2)}
+
+ ) + + return result + } +}) diff --git a/lib/agents/tools/mapbox/geocoding.tsx b/lib/agents/tools/mapbox/geocoding.tsx new file mode 100644 index 00000000..bc0644ce --- /dev/null +++ b/lib/agents/tools/mapbox/geocoding.tsx @@ -0,0 +1,73 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { BotMessage } from '@/components/message' +import { Card } from '@/components/ui/card' +import { geocodingSchema } from '@/lib/schema/mapbox' +import { getConnectedMcpClient, closeClient } from './mcp-client' +import { ToolProps } from '..' + +export const mapboxGeocodingTool = ({ uiStream }: ToolProps) => ({ + description: 'Get coordinates for a location and optionally a map.', + parameters: geocodingSchema, + execute: async ({ + query, + includeMap + }: { + query: string + includeMap: boolean + }) => { + const uiFeedbackStream = createStreamableValue() + uiStream.append() + + uiFeedbackStream.update(`Searching for "${query}"...`) + + const mcpClient = await getConnectedMcpClient() + if (!mcpClient) { + const error = + 'Mapbox tool is not available. Please check your configuration.' + uiFeedbackStream.update(error) + return { error } + } + + let result + try { + const toolArgs = { query, includeMapPreview: includeMap } + const geocodeResult = await mcpClient.callTool({ + name: 'mapbox_geocoding', + arguments: toolArgs + }) + + const toolResults = (geocodeResult as any)?.tool_results || [] + if (toolResults.length === 0 || !toolResults[0]?.content) { + throw new Error('No content returned from mapping service') + } + + let content = toolResults[0].content + if (typeof content === 'string') { + const jsonRegex = /```(?:json)?\n?([\s\S]*?)\n?```/ + const match = content.match(jsonRegex) + if (match) { + content = JSON.parse(match[1].trim()) + } else { + content = JSON.parse(content) + } + } + result = content + } catch (error: any) { + console.error('Mapbox geocoding tool error:', error) + const errorMessage = `Error searching for "${query}": ${error.message}` + uiFeedbackStream.update(errorMessage) + result = { error: errorMessage } + } finally { + await closeClient(mcpClient) + } + + uiFeedbackStream.done() + uiStream.update( + +
{JSON.stringify(result, null, 2)}
+
+ ) + + return result + } +}) diff --git a/lib/agents/tools/mapbox/isochrone.tsx b/lib/agents/tools/mapbox/isochrone.tsx new file mode 100644 index 00000000..9cb1522b --- /dev/null +++ b/lib/agents/tools/mapbox/isochrone.tsx @@ -0,0 +1,77 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { BotMessage } from '@/components/message' +import { Card } from '@/components/ui/card' +import { isochroneSchema } from '@/lib/schema/mapbox' +import { getConnectedMcpClient, closeClient } from './mcp-client' +import { ToolProps } from '..' + +export const mapboxIsochroneTool = ({ uiStream }: ToolProps) => ({ + description: 'Generate isochrone polygons to show areas reachable within a certain time.', + parameters: isochroneSchema, + execute: async ({ + location, + contour_minutes, + profile + }: { + location: string + contour_minutes: number + profile: 'driving' | 'walking' | 'cycling' + }) => { + const uiFeedbackStream = createStreamableValue() + uiStream.append() + + uiFeedbackStream.update( + `Generating ${contour_minutes}-minute isochrone for "${location}"...` + ) + + const mcpClient = await getConnectedMcpClient() + if (!mcpClient) { + const error = + 'Mapbox tool is not available. Please check your configuration.' + uiFeedbackStream.update(error) + return { error } + } + + let result + try { + const toolArgs = { location, contour_minutes, profile } + const isochroneResult = await mcpClient.callTool({ + name: 'mapbox_isochrone', + arguments: toolArgs + }) + + const toolResults = (isochroneResult as any)?.tool_results || [] + if (toolResults.length === 0 || !toolResults[0]?.content) { + throw new Error('No content returned from mapping service') + } + + let content = toolResults[0].content + if (typeof content === 'string') { + const jsonRegex = /```(?:json)?\n?([\s\S]*?)\n?```/ + const match = content.match(jsonRegex) + if (match) { + content = JSON.parse(match[1].trim()) + } else { + content = JSON.parse(content) + } + } + result = content + } catch (error: any) { + console.error('Mapbox isochrone tool error:', error) + const errorMessage = `Error generating isochrone: ${error.message}` + uiFeedbackStream.update(errorMessage) + result = { error: errorMessage } + } finally { + await closeClient(mcpClient) + } + + uiFeedbackStream.done() + uiStream.update( + +
{JSON.stringify(result, null, 2)}
+
+ ) + + return result + } +}) diff --git a/lib/agents/tools/mapbox/matrix.tsx b/lib/agents/tools/mapbox/matrix.tsx new file mode 100644 index 00000000..601dc248 --- /dev/null +++ b/lib/agents/tools/mapbox/matrix.tsx @@ -0,0 +1,77 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { BotMessage } from '@/components/message' +import { Card } from '@/components/ui/card' +import { matrixSchema } from '@/lib/schema/mapbox' +import { getConnectedMcpClient, closeClient } from './mcp-client' +import { ToolProps } from '..' + +export const mapboxMatrixTool = ({ uiStream }: ToolProps) => ({ + description: 'Calculate travel times between multiple origins and destinations.', + parameters: matrixSchema, + execute: async ({ + origins, + destinations, + profile + }: { + origins: string[] + destinations: string[] + profile: 'driving' | 'walking' | 'cycling' + }) => { + const uiFeedbackStream = createStreamableValue() + uiStream.append() + + uiFeedbackStream.update( + `Calculating travel times for ${origins.length} origins to ${destinations.length} destinations...` + ) + + const mcpClient = await getConnectedMcpClient() + if (!mcpClient) { + const error = + 'Mapbox tool is not available. Please check your configuration.' + uiFeedbackStream.update(error) + return { error } + } + + let result + try { + const toolArgs = { origins, destinations, profile } + const matrixResult = await mcpClient.callTool({ + name: 'mapbox_matrix', + arguments: toolArgs + }) + + const toolResults = (matrixResult as any)?.tool_results || [] + if (toolResults.length === 0 || !toolResults[0]?.content) { + throw new Error('No content returned from mapping service') + } + + let content = toolResults[0].content + if (typeof content === 'string') { + const jsonRegex = /```(?:json)?\n?([\s\S]*?)\n?```/ + const match = content.match(jsonRegex) + if (match) { + content = JSON.parse(match[1].trim()) + } else { + content = JSON.parse(content) + } + } + result = content + } catch (error: any) { + console.error('Mapbox matrix tool error:', error) + const errorMessage = `Error calculating travel times: ${error.message}` + uiFeedbackStream.update(errorMessage) + result = { error: errorMessage } + } finally { + await closeClient(mcpClient) + } + + uiFeedbackStream.done() + uiStream.update( + +
{JSON.stringify(result, null, 2)}
+
+ ) + + return result + } +}) diff --git a/lib/agents/tools/mapbox/mcp-client.ts b/lib/agents/tools/mapbox/mcp-client.ts new file mode 100644 index 00000000..70dc7ecf --- /dev/null +++ b/lib/agents/tools/mapbox/mcp-client.ts @@ -0,0 +1,97 @@ +import { Client as MCPClientClass } from '@modelcontextprotocol/sdk/client/index.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { createSmitheryUrl } from '@smithery/sdk' + +export type McpClient = MCPClientClass + +export async function getConnectedMcpClient(): Promise { + const apiKey = process.env.NEXT_PUBLIC_SMITHERY_API_KEY + const mapboxAccessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN + const profileId = process.env.NEXT_PUBLIC_SMITHERY_PROFILE_ID + + if (!apiKey || !mapboxAccessToken || !profileId) { + console.error('[MCP-Client] Missing required environment variables') + return null + } + + let config + try { + const mapboxMcpConfig = await import('../../../../mapbox_mcp_config.json') + config = { + ...(mapboxMcpConfig.default || mapboxMcpConfig), + mapboxAccessToken + } + } catch (configError: any) { + console.error( + '[MCP-Client] Failed to load mapbox config:', + configError.message + ) + config = { + mapboxAccessToken, + version: '1.0.0', + name: 'mapbox-mcp-server' + } + } + + const smitheryUrlOptions = { config, apiKey, profileId } + const mcpServerBaseUrl = `https://server.smithery.ai/mapbox-mcp-server/mcp?api_key=${smitheryUrlOptions.apiKey}&profile=${smitheryUrlOptions.profileId}` + + let serverUrlToUse + try { + serverUrlToUse = createSmitheryUrl(mcpServerBaseUrl, smitheryUrlOptions) + } catch (urlError: any) { + console.error('[MCP-Client] Error creating Smithery URL:', urlError.message) + return null + } + + let transport + try { + transport = new StreamableHTTPClientTransport(serverUrlToUse) + } catch (transportError: any) { + console.error( + '[MCP-Client] Failed to create transport:', + transportError.message + ) + return null + } + + const client = new MCPClientClass({ + name: 'MapboxToolClient', + version: '1.0.0' + }) + + try { + await Promise.race([ + client.connect(transport), + new Promise((_, reject) => { + setTimeout( + () => reject(new Error('Connection timeout after 15 seconds')), + 15000 + ) + }) + ]) + return client + } catch (connectionError: any) { + console.error('[MCP-Client] MCP connection failed:', connectionError.message) + await closeClient(client) + return null + } +} + +export async function closeClient(client: MCPClientClass | null) { + if (!client) return + + try { + await Promise.race([ + client.close(), + new Promise((_, reject) => { + setTimeout( + () => reject(new Error('Close timeout after 5 seconds')), + 5000 + ) + }) + ]) + } catch (error: any) { + console.error('[MCP-Client] Error closing MCP client:', error.message) + } +} diff --git a/lib/agents/tools/mapbox/static-image.tsx b/lib/agents/tools/mapbox/static-image.tsx new file mode 100644 index 00000000..d70e18cd --- /dev/null +++ b/lib/agents/tools/mapbox/static-image.tsx @@ -0,0 +1,87 @@ +import { createStreamableUI, createStreamableValue } from 'ai/rsc' +import { BotMessage } from '@/components/message' +import { Card } from '@/components/ui/card' +import { staticImageSchema } from '@/lib/schema/mapbox' +import { getConnectedMcpClient, closeClient } from './mcp-client' +import { ToolProps } from '..' + +export const mapboxStaticImageTool = ({ uiStream }: ToolProps) => ({ + description: 'Generate a static map image.', + parameters: staticImageSchema, + execute: async ({ + center, + zoom, + width, + height + }: { + center: string + zoom: number + width: number + height: number + }) => { + const uiFeedbackStream = createStreamableValue() + uiStream.append() + + uiFeedbackStream.update( + `Generating a ${width}x${height} map image centered at ${center}...` + ) + + const mcpClient = await getConnectedMcpClient() + if (!mcpClient) { + const error = + 'Mapbox tool is not available. Please check your configuration.' + uiFeedbackStream.update(error) + return { error } + } + + let result + try { + const toolArgs = { center, zoom, width, height } + const staticImageResult = await mcpClient.callTool({ + name: 'mapbox_static_image', + arguments: toolArgs + }) + + const toolResults = (staticImageResult as any)?.tool_results || [] + if (toolResults.length === 0 || !toolResults[0]?.content) { + throw new Error('No content returned from mapping service') + } + + let content = toolResults[0].content + if (typeof content === 'string') { + const jsonRegex = /```(?:json)?\n?([\s\S]*?)\n?```/ + const match = content.match(jsonRegex) + if (match) { + content = JSON.parse(match[1].trim()) + } else { + content = JSON.parse(content) + } + } + result = content + } catch (error: any) { + console.error('Mapbox static image tool error:', error) + const errorMessage = `Error generating map image: ${error.message}` + uiFeedbackStream.update(errorMessage) + result = { error: errorMessage } + } finally { + await closeClient(mcpClient) + } + + uiFeedbackStream.done() + if (result.imageUrl) { + uiStream.update( + + Static Map + + ) + } else { + uiStream.update( + +
{JSON.stringify(result, null, 2)}
+
+ ) + } + + return result + } +}) diff --git a/lib/schema/mapbox.ts b/lib/schema/mapbox.ts new file mode 100644 index 00000000..74572f30 --- /dev/null +++ b/lib/schema/mapbox.ts @@ -0,0 +1,57 @@ +import { z } from 'zod' + +export const geocodingSchema = z.object({ + query: z.string().describe('The address or place name to geocode.'), + includeMap: z + .boolean() + .optional() + .default(true) + .describe('Whether to include a map preview in the result.') +}) + +export const directionsSchema = z.object({ + origin: z.string().describe('The starting point for the directions.'), + destination: z.string().describe('The ending point for the directions.'), + profile: z + .enum(['driving', 'walking', 'cycling']) + .optional() + .default('driving') + .describe('The mode of transportation.') +}) + +export const matrixSchema = z.object({ + origins: z + .array(z.string()) + .describe('An array of starting points, as addresses or coordinates.'), + destinations: z + .array(z.string()) + .describe('An array of ending points, as addresses or coordinates.'), + profile: z + .enum(['driving', 'walking', 'cycling']) + .optional() + .default('driving') + .describe('The mode of transportation.') +}) + +export const isochroneSchema = z.object({ + location: z + .string() + .describe('The center point for the isochrone, as an address or coordinates.'), + contour_minutes: z + .number() + .describe('The time in minutes to calculate the reachable area.'), + profile: z + .enum(['driving', 'walking', 'cycling']) + .optional() + .default('driving') + .describe('The mode of transportation.') +}) + +export const staticImageSchema = z.object({ + center: z + .string() + .describe('The center of the map, as longitude,latitude.'), + zoom: z.number().describe('The zoom level of the map.'), + width: z.number().describe('The width of the image in pixels.'), + height: z.number().describe('The height of the image in pixels.') +}) diff --git a/mapbox_mcp/hooks.ts b/mapbox_mcp/hooks.ts index 6e295609..f1848685 100644 --- a/mapbox_mcp/hooks.ts +++ b/mapbox_mcp/hooks.ts @@ -8,7 +8,7 @@ type Tool = { name: string; // Add other properties as needed based on your usage }; -import { getModel } from 'QCX/lib/utils'; +import { getModel } from '../lib/utils'; // Types for location and mapping data interface LocationResult { diff --git a/package.json b/package.json index 56178678..36319579 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", - "QCX": ".", "ai": "^4.3.16", "build": "^0.1.4", "class-variance-authority": "^0.7.1",