index.ts•7.41 kB
#!/usr/bin/env node
/**
* Pinecone MCP Server
* Provides tools for interacting with Pinecone vector database
* - Query vectors with semantic search
* - Upsert new vectors
* - Delete vectors
* - Get index statistics
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// @ts-ignore
import { Pinecone } from "pinecone";
import OpenAI from "openai";
// Environment variables
const PINECONE_API_KEY = process.env.PINECONE_API_KEY;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const INDEX_NAME = process.env.PINECONE_INDEX_NAME || "ad-assessor-docs";
if (!PINECONE_API_KEY) {
throw new Error("PINECONE_API_KEY environment variable is required");
}
if (!OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY environment variable is required");
}
// Initialize clients
const pc = new Pinecone({ apiKey: PINECONE_API_KEY });
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
/**
* Create an MCP server with tools for Pinecone operations
*/
const server = new Server(
{
name: "pinecone-mcp",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
/**
* Get embedding for text using OpenAI
*/
async function getEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
input: text,
model: "text-embedding-ada-002"
});
return response.data[0].embedding;
}
/**
* Handler that lists available tools for Pinecone operations
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_vectors",
description: "Perform semantic search on the Pinecone vector database",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Text query to search for similar content"
},
top_k: {
type: "number",
description: "Number of results to return (default: 5)",
default: 5
},
include_metadata: {
type: "boolean",
description: "Whether to include metadata in results (default: true)",
default: true
}
},
required: ["query"]
}
},
{
name: "upsert_vectors",
description: "Add new vectors to the Pinecone database",
inputSchema: {
type: "object",
properties: {
texts: {
type: "array",
items: { type: "string" },
description: "Array of text strings to embed and store"
},
metadatas: {
type: "array",
items: { type: "object" },
description: "Array of metadata objects for each text"
},
ids: {
type: "array",
items: { type: "string" },
description: "Array of IDs for the vectors (optional, auto-generated if not provided)"
}
},
required: ["texts"]
}
},
{
name: "delete_vectors",
description: "Delete vectors from the Pinecone database",
inputSchema: {
type: "object",
properties: {
ids: {
type: "array",
items: { type: "string" },
description: "Array of vector IDs to delete"
},
delete_all: {
type: "boolean",
description: "Delete all vectors in the index (use with caution)",
default: false
}
}
}
},
{
name: "get_index_stats",
description: "Get statistics about the Pinecone index",
inputSchema: {
type: "object",
properties: {}
}
}
]
};
});
/**
* Handler for tool calls
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const index = pc.index(INDEX_NAME);
switch (request.params.name) {
case "query_vectors": {
const query = String(request.params.arguments?.query);
const topK = Number(request.params.arguments?.top_k) || 5;
const includeMetadata = request.params.arguments?.include_metadata !== false;
if (!query) {
throw new Error("Query is required");
}
// Get embedding for the query
const queryEmbedding = await getEmbedding(query);
// Query Pinecone
const queryResponse = await index.query({
vector: queryEmbedding,
topK,
includeMetadata
});
return {
content: [{
type: "text",
text: JSON.stringify(queryResponse, null, 2)
}]
};
}
case "upsert_vectors": {
const texts = request.params.arguments?.texts as string[];
const metadatas = request.params.arguments?.metadatas as object[] | undefined;
const ids = request.params.arguments?.ids as string[] | undefined;
if (!texts || !Array.isArray(texts) || texts.length === 0) {
throw new Error("Texts array is required and must not be empty");
}
// Generate embeddings for all texts
const embeddings = await Promise.all(texts.map(text => getEmbedding(text)));
// Prepare vectors for upsert
const vectors = texts.map((text, i) => ({
id: ids?.[i] || `vec_${Date.now()}_${i}`,
values: embeddings[i],
metadata: {
text,
...metadatas?.[i]
}
}));
// Upsert to Pinecone
const upsertResponse = await index.upsert(vectors);
return {
content: [{
type: "text",
text: `Successfully upserted ${vectors.length} vectors. Response: ${JSON.stringify(upsertResponse)}`
}]
};
}
case "delete_vectors": {
const ids = request.params.arguments?.ids as string[] | undefined;
const deleteAll = Boolean(request.params.arguments?.delete_all);
if (deleteAll) {
const deleteResponse = await index.deleteAll();
return {
content: [{
type: "text",
text: `Deleted all vectors from index. Response: ${JSON.stringify(deleteResponse)}`
}]
};
}
if (!ids || !Array.isArray(ids) || ids.length === 0) {
throw new Error("IDs array is required for selective deletion");
}
const deleteResponse = await index.deleteMany(ids);
return {
content: [{
type: "text",
text: `Deleted ${ids.length} vectors. Response: ${JSON.stringify(deleteResponse)}`
}]
};
}
case "get_index_stats": {
const stats = await index.describeIndexStats();
return {
content: [{
type: "text",
text: JSON.stringify(stats, null, 2)
}]
};
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
});
/**
* Start the server using stdio transport
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Pinecone MCP server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});