From 5e78e7183cc4c960fb0fe8ea831a33187e37a77c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 06:06:49 +0000 Subject: [PATCH 1/3] Initial plan for issue From ec24f60e0ed6eebd510dac1d1d8fa6120a4eea0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 06:11:10 +0000 Subject: [PATCH 2/3] Implement speaker fetch tool Co-authored-by: 0GiS0 <175379+0GiS0@users.noreply.github.com> --- codemotion-mcp-server/src/index.ts | 40 +++--- .../src/tools/speakerTool.ts | 133 ++++++++++++++++++ 2 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 codemotion-mcp-server/src/tools/speakerTool.ts diff --git a/codemotion-mcp-server/src/index.ts b/codemotion-mcp-server/src/index.ts index 6a1fd3f..da6c32b 100644 --- a/codemotion-mcp-server/src/index.ts +++ b/codemotion-mcp-server/src/index.ts @@ -1,18 +1,19 @@ -import express from 'express'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { randomUUID } from "node:crypto"; -import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; - - -//load env variables -import dotenv from 'dotenv'; - -// Tools -import { registerTimeTool } from './tools/timeTool.js'; -import { registerSessionTool } from './tools/sessionTool.js'; - - +import express from 'express'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { randomUUID } from "node:crypto"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; + + +//load env variables +import dotenv from 'dotenv'; + +// Tools +import { registerTimeTool } from './tools/timeTool.js'; +import { registerSessionTool } from './tools/sessionTool.js'; +import { registerSpeakerTool } from './tools/speakerTool.js'; + + dotenv.config(); /** @@ -65,10 +66,11 @@ app.post('/mcp', async (req, res) => { version: '1.0.0' }); - // Add tools - registerTimeTool(server); - registerSessionTool(server); - + // Add tools + registerTimeTool(server); + registerSessionTool(server); + registerSpeakerTool(server); + await server.connect(transport); } else { // Invalid request diff --git a/codemotion-mcp-server/src/tools/speakerTool.ts b/codemotion-mcp-server/src/tools/speakerTool.ts new file mode 100644 index 0000000..2783044 --- /dev/null +++ b/codemotion-mcp-server/src/tools/speakerTool.ts @@ -0,0 +1,133 @@ +import { z } from 'zod'; +import { QdrantClient } from '@qdrant/js-client-rest'; +import OpenAI from 'openai'; + +/** + * Registers the speakers tool in the MCP server. + * + * @param server - The MCP server instance to register the tool with. + */ +export const registerSpeakerTool = (server: any): void => { + // Get speakers from Codemotion agenda in Qdrant + server.tool( + 'speakers', + 'Get the list of speakers from Codemotion agenda', + { + query: z.string().min(1).max(100).optional() + }, + /** + * Handler for fetching speakers from Qdrant based on an optional query. + * @param query - Optional query string to search speakers. + * @returns An object containing the formatted speaker results or an error message. + */ + async ({ query }: { query?: string }) => { + const chalk = (await import('chalk')).default; + console.log(chalk.blue('Codemotion MCP Server: Speakers tool called')); + console.log(chalk.blue('Codemotion MCP Server: Query:', query)); + + try { + // Create a Qdrant client + const qdrantClient = new QdrantClient({ + host: 'qdrant', + port: 6333, + https: false, + checkCompatibility: false + }); + + let searchResponse; + + // If there's a query, use vector search + if (query) { + console.log(chalk.blue('Vectorizing the query...')); + + // Create OpenAI client + const openaiClient = new OpenAI({ + apiKey: process.env.GITHUB_TOKEN, + baseURL: process.env.GITHUB_MODELS_URL, + }); + + // Generate the embedding for the query + const response = await openaiClient.embeddings.create({ + model: process.env.GITHUB_MODELS_MODEL_FOR_EMBEDDINGS || 'text-embedding-3-large', + input: query + }); + + // Get the vector from the response + const vector = response.data[0].embedding; + console.log(chalk.blue('Codemotion MCP Server: Vector created')); + console.log(chalk.blue('Codemotion MCP Server: Searching for speakers in Qdrant...')); + + // Search for the sessions in Qdrant using vector search + searchResponse = await qdrantClient.query( + process.env.QDRANT_COLLECTION_NAME || 'codemotion', + { + query: vector, + limit: 10, + with_payload: true + } + ); + } else { + // If no query provided, get all speakers via scroll API + console.log(chalk.blue('Codemotion MCP Server: No query provided, fetching all speakers...')); + + searchResponse = await qdrantClient.scroll( + process.env.QDRANT_COLLECTION_NAME || 'codemotion', + { + limit: 100, + with_payload: true + } + ); + } + + console.log(chalk.blue('Codemotion MCP Server: Search response received')); + + // Extract unique speakers from the search results + const points = query ? searchResponse.points : searchResponse.points; + const speakersSet = new Set(); + + points.forEach((point: any) => { + const speakersData = point.payload?.speakers; + if (speakersData) { + if (typeof speakersData === 'string' && speakersData.trim() !== '') { + // If speakers is a single string, split by commas (as it might be comma-separated) + speakersData.split(', ').forEach(speaker => { + if (speaker.trim() !== '') { + speakersSet.add(speaker.trim()); + } + }); + } + } + }); + + // Convert Set to Array and sort alphabetically + const speakersArray = Array.from(speakersSet).sort(); + + console.log(chalk.blue(`Codemotion MCP Server: ${speakersArray.length} unique speakers found`)); + + // Format the result + const speakersList = speakersArray.map(speaker => `- ${speaker}`).join('\n'); + const result = `Speakers from Codemotion agenda${query ? ` matching "${query}"` : ''}:\n${speakersList}\n`; + + return { + content: [ + { + type: 'text', + text: result + } + ] + }; + } catch (error: any) { + console.dir(error, { depth: null }); + console.error(chalk.red('Codemotion MCP Server: Error fetching speakers from Qdrant:', error)); + return { + content: [ + { + type: 'text', + text: `Error fetching speakers from Qdrant: ${error?.message || 'Unknown error'}` + } + ] + }; + } + } + ); +}; \ No newline at end of file From b7c2781db4c2adc60383c7e0b798daeaef01541a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 06:13:31 +0000 Subject: [PATCH 3/3] Update speaker tool and add documentation Co-authored-by: 0GiS0 <175379+0GiS0@users.noreply.github.com> --- README.md | 11 +++++-- .../src/tools/speakerTool.ts | 29 ++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index abb63d1..64358ab 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,14 @@ Para consultar los datos de la agenda he creado un script en Python que utiliza ## Misión 4: Crear un MCP Server para que podamos integrar estas consultas como parte de Github Copilot Chat -Para nuestra cuarta mision he creado un MCP Server que se encuentra en el direction `codemotion-mcp-server`. Este servidor se encarga de recibir las consultasdel usuario, que le llegan a través de GitHub Copilot Chat para buscar en esa base de datos vectorial las charlas que más se ajustan a la consulta del usuario. También se encarga de darle la hora y el día actual para que le de información de las charlas que se están dando en ese momento o que se van a dar en el futuro. - - +Para nuestra cuarta mision he creado un MCP Server que se encuentra en el direction `codemotion-mcp-server`. Este servidor se encarga de recibir las consultasdel usuario, que le llegan a través de GitHub Copilot Chat para buscar en esa base de datos vectorial las charlas que más se ajustan a la consulta del usuario. También se encarga de darle la hora y el día actual para que le de información de las charlas que se están dando en ese momento o que se van a dar en el futuro. + +El servidor proporciona las siguientes herramientas: + +- **time**: Obtener la hora actual, opcionalmente con una zona horaria específica +- **sessions**: Buscar sesiones de la Codemotion basadas en una consulta y fecha opcional +- **speakers**: Obtener la lista de todos los ponentes o filtrar según una consulta específica + Puedes probar el servidor usando la herramienta MCP Inspector: ```bash diff --git a/codemotion-mcp-server/src/tools/speakerTool.ts b/codemotion-mcp-server/src/tools/speakerTool.ts index 2783044..b2d92a5 100644 --- a/codemotion-mcp-server/src/tools/speakerTool.ts +++ b/codemotion-mcp-server/src/tools/speakerTool.ts @@ -86,16 +86,25 @@ export const registerSpeakerTool = (server: any): void => { const speakersSet = new Set(); points.forEach((point: any) => { - const speakersData = point.payload?.speakers; - if (speakersData) { - if (typeof speakersData === 'string' && speakersData.trim() !== '') { - // If speakers is a single string, split by commas (as it might be comma-separated) - speakersData.split(', ').forEach(speaker => { - if (speaker.trim() !== '') { - speakersSet.add(speaker.trim()); - } - }); - } + // Handle speakers array + if (Array.isArray(point.payload?.speakers)) { + point.payload.speakers.forEach((speaker: string) => { + if (speaker && speaker.trim() !== '') { + speakersSet.add(speaker.trim()); + } + }); + } + // Handle speakers as string (comma-separated) + else if (typeof point.payload?.speakers === 'string' && point.payload.speakers.trim() !== '') { + point.payload.speakers.split(', ').forEach(speaker => { + if (speaker.trim() !== '') { + speakersSet.add(speaker.trim()); + } + }); + } + // Handle individual speaker field + else if (typeof point.payload?.speaker === 'string' && point.payload.speaker.trim() !== '') { + speakersSet.add(point.payload.speaker.trim()); } });