• Uncategorised

Building your own MCP server

RAG Pinecone MCP Server

This server implements the Model Context Protocol (MCP) for RAG (Retrieval-Augmented Generation) using Pinecone as the vector database.

Environment Variables

Required environment variables in your .env file:

PINECONE_API_KEY=           # Your Pinecone API key. Get here https://www.pinecone.io/
OPENAI_API_KEY=             # Your OpenAI API key. Get here https://platform.openai.com/api-keys
PINECONE_NAMESPACE=ns1      # Namespace for your vectors
PINECONE_INDEX_NAME=        # Name of your Pinecone index. Create index in Pinecone
PINECONE_ENVIRONMENT=       # Pinecone environment (e.g., us-east-1)
DESCRIPTION=                # Description for your RAG queries

Configuring as MCP Server in Cursor

Add the following configuration to your ~/.cursor/mcp.json file:

{
  "mcpServers": {
    "pinecone": {
      "command": "node",
      "args": [
        "/path/to/adobe-mcp-servers/src/pinecone/dist/server.js"
      ],
      "env": {
        "OPENAI_API_KEY": "your_openai_api_key",
        "PINECONE_API_KEY": "your_pinecone_api_key",
        "PINECONE_NAMESPACE": "ns1",
        "PINECONE_INDEX_NAME": "your_index_name",
        "PINECONE_ENVIRONMENT": "us-east-1",
        "DESCRIPTION": "Your RAG query description"
      }
    }
  }
}

Make sure to:

  1. Replace /path/to/ with the actual path to your server.js file
  2. Update all API keys and configuration values
  3. Restart Cursor after making changes to mcp.json

Available MCP Tools

This server provides two main tools:

1. index_doc

Indexes documents into Pinecone for later retrieval.

Input Parameters:

  • document: Either a file path or direct text content to index
    • For files: Provide the full path (e.g., “/Users/name/Documents/example.txt”)
    • For text: Provide the content directly as a string

Features:

  • Automatic text chunking for optimal indexing
  • Support for text and DOCX files
  • Generates embeddings using OpenAI’s text-embedding-ada-002 model
  • Stores vectors in specified Pinecone namespace

Example Content to Index:

ColdFusion Scheduler Date Range Exclusion Example:
Exclude attribute can be used to exclude dates from scheduler. This example shows how to specify a date range to be excluded.

<cfset excludedatestart = dateformat(dateadd("d",-10,startdate),"mm/dd/yyyy")>
<cfset excludedateend = dateformat(dateadd("d",10,startdate),"mm/dd/yyyy")>
<cfschedule action="update" task="exclude_task" operation="HTTPRequest" 
            URL="#TaskURL#" startdate="#startdate#" starttime="#starttime#" 
            interval="1" exclude="#excludedatestart# to #excludedateend#">

2. rag_query

Performs semantic search over indexed documents.

Input Parameters:

  • query: The search query text

Features:

  • Semantic search using OpenAI embeddings
  • Returns relevant document chunks with similarity scores
  • Searches within specified Pinecone namespace
  • Returns metadata about matched documents

Example Queries:

  • “How do I exclude date ranges in ColdFusion scheduler?”
  • “What is the syntax for cfschedule exclude attribute?”
  • “How to skip task execution for specific dates in ColdFusion?”
  • “Show me examples of date range exclusion in CF scheduler”

Building and Running

  1. Build the project:
npm run build
  1. Start the server:
npm run start

Features

  • Document indexing with automatic chunking
  • Support for text and DOCX files
  • RAG queries using OpenAI embeddings
  • Integration with Pinecone vector database
  • MCP-compliant server implementation

Error Handling

The server includes comprehensive error handling for:

  • File reading errors
  • Document processing errors
  • API errors (OpenAI, Pinecone)
  • Invalid requests
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema, Tool } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import dotenv from "dotenv";
import { Pinecone } from '@pinecone-database/pinecone';
import OpenAI from 'openai';
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import mammoth from 'mammoth';

dotenv.config();

let isServerRunning = false;
let transport: StdioServerTransport | null = null;

// Server setup
const server = new Server(
  {
     name: "RAG_Pinecone",
     version: "1.0.0"
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

// Initialize OpenAI
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY || ''
});

// Initialize Pinecone
const pinecone = new Pinecone({
  apiKey: process.env.PINECONE_API_KEY || ''
});

// Get the index name from environment
const indexName = process.env.PINECONE_INDEX_NAME || 'myindex';

// Function to list all available indexes
async function listIndexes() {
  try {
    const indexes = await pinecone.listIndexes();
    return indexes;
  } catch (error) {
    console.error('Error listing indexes:', error);
    throw error;
  }
}

// Function to check if an index exists
async function indexExists(name: string): Promise<boolean> {
  try {
    const indexes = await pinecone.listIndexes();
    return indexes?.indexes?.some(index => index.name === name) ?? false;
  } catch (error) {
    console.error(`Error checking index ${name}:`, error);
    return false;
  }
}

// Function to create a new index
async function createIndex(name: string) {
  try {
    // Check if index already exists
    const exists = await indexExists(name);
    if (exists) {
      return;
    }

    // Create new index with OpenAI ada-002 dimensions (1536)
    await pinecone.createIndex({
      name,
      dimension: 1536,
      metric: 'cosine',
      spec: {
        serverless: {
          cloud: 'aws',
          region: process.env.PINECONE_ENVIRONMENT || 'us-east-1'
        }
      }
    });

    // Simple wait for 3 seconds
    await new Promise(resolve => setTimeout(resolve, 3000));
  } catch (error) {
    console.error(`Error creating index ${name}:`, error);
    throw error;
  }
}

// Initialize index on startup
async function initializeIndex() {
  try {
    const exists = await indexExists(indexName);
    if (!exists) {
      await createIndex(indexName);
    }
  } catch (error) {
    console.error('Error initializing index:', error);
    throw error;
  }
}

// Call initialization when server starts
initializeIndex().catch(error => {
  console.error('Failed to initialize Pinecone index:', error);
  process.exit(1);
});

const description = process.env.DESCRIPTION ?? "The user's query related to documents indexed in Pinecone";

export const INDEX_DOC_TOOL: Tool = {
  name: "index_document",
  description: "Index documents in Pinecone",
  inputSchema: {
    type: "object",
    properties: {
      document: { type: "string", description: "Document to index in pine cone" },
    },
  },
};

export const RAG_QUERY_TOOL: Tool = {
  name: "rag_query",
  description: description,
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string", description: description },
    },
  },
};

// Request handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
   INDEX_DOC_TOOL,
   RAG_QUERY_TOOL
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    let result;
    switch (name) {
      case "index_document":
        result = await indexDoc(args);
        break;
      case "rag_query":
        result = await ragQuery(args);
        break;
      
      default:
        return {
          content: [{ type: "text", text: `Unknown tool: ${name}` }],
          isError: true,
        };
    }

    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
    };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error occurred: ${error}` }],
      isError: true,
    };
  }
});

// Function to generate embeddings
async function generateEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: "text-embedding-ada-002",
    input: text,
  });
  return response.data[0].embedding;
}

// Define coldfusionexpert tool schema
const RAGQuerySchema = z.object({
  query: z.string().describe(description)
});

// Define coldfusionexpert tool schema
const indexDocsSchema = z.object({
  document: z.string().describe("Document to index in pine cone")
});

type RAGQueryRequest = z.infer<typeof RAGQuerySchema>;
type indexDocsRequest = z.infer<typeof indexDocsSchema>;


// Modified server startup
async function startServer() {
  if (isServerRunning) {
   // console.log('Server is already running');
    return;
  }

  try {
   // console.log('Starting MCP server...');
    
    // Initialize transport before process.stdin.resume()
    transport = new StdioServerTransport();
    
    // Keep the process alive
    process.stdin.resume();
    
    // Connect server with transport
    await server.connect(transport);
    
    isServerRunning = true;
   // console.log('MCP server connected successfully');

    // Handle process termination
    const cleanup = async () => {
      if (isServerRunning) {
        console.log('Terminating MCP server...');
        isServerRunning = false;
        if (transport) {
          transport.close();
          transport = null;
        }
        // Don't exit immediately to allow cleanup
        setTimeout(() => process.exit(0), 100);
      }
    };

    // Handle process signals
    process.on('SIGINT', cleanup);
    process.on('SIGTERM', cleanup);
    
    // Handle uncaught errors
    process.on('uncaughtException', async (error) => {
      console.error('Uncaught exception:', error);
      await cleanup();
    });

    // Handle unhandled rejections
    process.on('unhandledRejection', async (error) => {
      console.error('Unhandled rejection:', error);
      await cleanup();
    });

  } catch (error) {
    console.error('Error starting MCP server:', error);
    process.exit(1);
  }
}

// Start the server
startServer(); 

async function extractPdfContent(pdfBuffer: Buffer): Promise<string> {
  // Simplified version - just return empty string for now
  return '';
}

function generateDocumentId(content: string, fileName?: string): string {
  if (fileName) {
    return fileName.replace(/\.[^/.]+$/, ''); // Remove extension
  }
  // Generate hash from content if no filename
  return createHash('md5').update(content).digest('hex').substring(0, 12);
}

function chunkContent(content: string, maxChunkSize: number = 4000): string[] {
  const words = content.split(/\s+/);
  const chunks: string[] = [];
  let currentChunk: string[] = [];
  let currentLength = 0;

  for (const word of words) {
    if (currentLength + word.length > maxChunkSize) {
      chunks.push(currentChunk.join(' '));
      currentChunk = [word];
      currentLength = word.length;
    } else {
      currentChunk.push(word);
      currentLength += word.length + 1; // +1 for space
    }
  }

  if (currentChunk.length > 0) {
    chunks.push(currentChunk.join(' '));
  }

  return chunks;
}

async function extractDocxContent(buffer: Buffer): Promise<string> {
  try {
    const result = await mammoth.extractRawText({ buffer });
    return result.value;
  } catch (error) {
    throw new Error(`Failed to extract DOCX content: ${error}`);
  }
}

async function indexDoc(params: any) {
  try {
    // First check if index exists
    const exists = await indexExists(indexName);
    if (!exists) {
      //console.log(`Index ${indexName} not found, creating...`);
      await createIndex(indexName);
    } else {
      //console.log(`Using existing index: ${indexName}`);
    }

    const { document } = params;
    let content: string;
    let documentId: string;
    //console.log('Document:', document);
    // Check if document is a file path
    if (typeof document === 'string' && document.startsWith('/')) {
      try {
        const buffer = readFileSync(document);
        if (document.toLowerCase().endsWith('.docx')) {
          content = await extractDocxContent(buffer);
        } else {
          content = buffer.toString('utf-8');
        }
        documentId = generateDocumentId(content, document.split('/').pop()); // Use filename from path
      } catch (fileError: unknown) {
        const errorMessage = fileError instanceof Error ? fileError.message : String(fileError);
        throw new Error(`Failed to process file: ${errorMessage}`);
      }
    } else {
      content = document;
      documentId = generateDocumentId(content);
    }

    // Split content into chunks
    const chunks = chunkContent(content);
    
    // Generate embeddings and upsert for each chunk
    const index = pinecone.index(indexName);
    const pineconeNamespace = index.namespace(process.env.PINECONE_NAMESPACE as string);
    
    const upsertPromises = chunks.map(async (chunk, idx) => {
      const queryEmbedding = await generateEmbedding(chunk);
      return pineconeNamespace.upsert([{
        id: `${documentId}_chunk_${idx}`,
        values: queryEmbedding,
        metadata: { 
          fullContent: chunk,
          sourceType: document.toLowerCase().endsWith('.docx') ? 'docx' : 'text',
          originalName: document,
          chunkIndex: idx,
          totalChunks: chunks.length
        }
      }]);
    });

    await Promise.all(upsertPromises);

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          result: "Success",
          documentId: documentId,
          chunksProcessed: chunks.length
        }, null, 2)
      }]
    };
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    return {
      content: [{
        type: "text",
        text: `Error processing query: ${errorMessage}`
      }]
    };
  }
}

async function ragQuery(params: any) {
  try {
    const { query } = params;
    // Generate embedding for the query
    const queryEmbedding = await generateEmbedding(query);
    const index = pinecone.index(indexName);
    const pineconeNamespace = index.namespace(process.env.PINECONE_NAMESPACE as string);
    // Query the index with the generated embedding
    const queryResponse = await pineconeNamespace.query({
      vector: queryEmbedding,
      topK: 5,
      includeMetadata: true
    });

    // Format the response
    const formattedMatches = queryResponse.matches?.map(match => ({
      score: match.score,
      metadata: match.metadata,
      text: match.metadata?.fullContent || 'No text available'
    })) || [];

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          query: query,
          matches: formattedMatches
        }, null, 2)
      }]
    };
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    return {
      content: [{
        type: "text",
        text: `Error processing query: ${errorMessage}`
      }]
    };
  }
}

You may also like...