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:
- Replace
/path/to/
with the actual path to your server.js file - Update all API keys and configuration values
- 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
- Build the project:
npm run build
- 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}`
}]
};
}
}