In this earlier post, we created a vector store in our Oracle Database 23ai and populated it with some content from Moby Dick. Since MCP is very popular these days, I thought it might be interesting to look how to create a very simple MCP server to expose the similarity search as and MCP tool.
Let’s jump right into it. First we are going to need a requirements.txt file with a list of the dependencies we need:
mcp>=1.0.0 oracledb langchain-community langchain-huggingface sentence-transformers pydantic And then go ahead and install these by running:
pip install -r requirements.txt Note: I used Python 3.12 and a virtual environment.
Now let’s create a file called mcp_server.py and get to work! Let’s start with some imports:
import asyncio import oracledb from mcp.server import Server from mcp.types import Tool, TextContent from pydantic import BaseModel from langchain_community.vectorstores import OracleVS from langchain_community.vectorstores.utils import DistanceStrategy from langchain_huggingface import HuggingFaceEmbeddings And we are going to need the details of the database so we can connect to that, so let’s define some variables to hold those parameters:
# Database connection parameters for Oracle Vector Store DB_USERNAME = "vector" DB_PASSWORD = "vector" DB_DSN = "localhost:1521/FREEPDB1" TABLE_NAME = "moby_dick_500_30" Note: These match the database and vector store used in the previous post.
Let’s create a function to connect to the database, and set up the embedding model and the vector store.
# Global variables for database connection and embedding model # These are initialized once on server startup for efficiency embedding_model = None # HuggingFace sentence transformer model vector_store = None # LangChain OracleVS wrapper for vector operations connection = None # Oracle database connection def initialize_db(): """ Initialize database connection and vector store This function is called once at server startup to establish: 1. Connection to Oracle database 2. HuggingFace embedding model (sentence-transformers/all-mpnet-base-v2) 3. LangChain OracleVS wrapper for vector similarity operations The embedding model converts text queries into 768-dimensional vectors that can be compared against pre-computed embeddings in the database. """ global embedding_model, vector_store, connection # Connect to Oracle database using oracledb driver connection = oracledb.connect( user=DB_USERNAME, password=DB_PASSWORD, dsn=DB_DSN ) # Initialize HuggingFace embeddings model # This model converts text to 768-dimensional vectors # Same model used to create the original embeddings in the database embedding_model = HuggingFaceEmbeddings( model_name="sentence-transformers/all-mpnet-base-v2" ) # Initialize vector store wrapper # OracleVS provides convenient interface for vector similarity operations vector_store = OracleVS( client=connection, table_name=TABLE_NAME, embedding_function=embedding_model, # Use cosine similarity for comparison distance_strategy=DistanceStrategy.COSINE, ) Again, note that I am using the same embedding model that we used to create the vectors in this vector store. This is important because we need to create embedding vectors for the queries using the same model, so that similarity comparisons will be valid. It’s also important that we use the right distance strategy – for text data, cosine is generally agreed to be the best option. For performance reasons, if we had created a vector index, we’d want to use the same algorithm so the index would be used when performing the search. Oracle will default to doing an “exact search” if there is no index and the algorithm does not match.
Now, let’s add a function to perform a query in our Moby Dick vector store, we’ll include a top-k parameter so the caller can specify how many results they want:
def search_moby_dick(query: str, k: int = 4) -> list[dict]: """ Perform vector similarity search on the moby_dick_500_30 table This function: 1. Converts the query text to a vector using the embedding model 2. Searches the database for the k most similar text chunks 3. Returns results ranked by similarity (cosine distance) Args: query: The search query text (natural language) k: Number of results to return (default: 4) Returns: List of dictionaries containing rank, content, and metadata for each result """ if vector_store is None: raise RuntimeError("Vector store not initialized") # Perform similarity search # The query is automatically embedded and compared against database vectors docs = vector_store.similarity_search(query, k=k) # Format results into structured dictionaries results = [] for i, doc in enumerate(docs): results.append({ "rank": i + 1, # 1-indexed ranking by similarity "content": doc.page_content, # The actual text chunk "metadata": doc.metadata # Headers from the original HTML structure }) return results As you can see, this function returns a dictionary containing the rank, the content (chunk) and the metadata.
Ok, now let’s turn this into an MCP server! First let’s create the server instance:
# Create MCP server instance # The server name "moby-dick-search" identifies this server in MCP client connections app = Server("moby-dick-search") Now we want to provide a list-tools method so that MCP clients can find out what kinds of tools this server provides. We are just going to have our search tool, so let’s define that:
@app.list_tools() async def list_tools() -> list[Tool]: """ MCP protocol handler: returns list of available tools Called by MCP clients to discover what capabilities this server provides. This server exposes a single tool: search_moby_dick Returns: List of Tool objects with names, descriptions, and input schemas """ return [ Tool( name="search_moby_dick", description="Search the Moby Dick text using vector similarity. Returns relevant passages based on semantic similarity to the query.", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "The search query text" }, "k": { "type": "integer", "description": "Number of results to return (default: 4)", "default": 4 } }, "required": ["query"] } ) ] And now, the part we’ve all been waiting for – let’s define the actual search tool (and a class to hold the arguments)!
class SearchArgs(BaseModel): """ Arguments for the vector search tool Attributes: query: The natural language search query k: Number of most similar results to return (default: 4) """ query: str k: int = 4 @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: """ MCP protocol handler: executes tool calls Called when an MCP client wants to use one of the server's tools. Validates the tool name, parses arguments, performs the search, and returns formatted results. Args: name: Name of the tool to call arguments: Dictionary of tool arguments Returns: List of TextContent objects containing the formatted search results """ # Validate tool name if name != "search_moby_dick": raise ValueError(f"Unknown tool: {name}") # Parse and validate arguments using Pydantic model args = SearchArgs(**arguments) # Perform the vector similarity search results = search_moby_dick(args.query, args.k) # Format response as human-readable text response_text = f"Found {len(results)} results for query: '{args.query}'\n\n" for result in results: response_text += f"--- Result {result['rank']} ---\n" response_text += f"Metadata: {result['metadata']}\n" response_text += f"Content: {result['content']}\n\n" # Return as MCP TextContent type return [TextContent(type="text", text=response_text)] That was not too bad. Finally, let’s set up a main function to start up everything and handle the requests:
async def main(): """ Main entry point for the MCP server This function: 1. Initializes the database connection and embedding model 2. Sets up stdio transport for MCP communication 3. Runs the server event loop to handle requests The server communicates via stdio (stdin/stdout), which allows it to be easily spawned by MCP clients as a subprocess. """ # Initialize database connection and models initialize_db() # Import stdio server transport from mcp.server.stdio import stdio_server # Run the server using stdio transport # The server reads MCP protocol messages from stdin and writes responses to stdout async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() ) if __name__ == "__main__": asyncio.run(main()) Ok, that’s it! We can run this with the command:
python mcp_server.py Now, to test it, we’re groing to need a client! MCP Inspector is the logical place to start, you can get it from here, or (assuming you have node installed) by just running this command:
npx @modelcontextprotocol/inspector python3.12 mcp_server.py That’s going to start up a UI that looks like this:

Click on the connect button, and you should see an updated screen in a few seconds that looks like this:

Go ahead and click on List Tools and you will see our Search Moby Dick Tool show up – click on it to try it out.

You should see some results like this:

There you go, it works great! And that’s a super simple, basic MCP server and tool! Enjoy.

Pingback: 讓我們製作一個簡單的 MCP 工具來 Oracle AI Vector Search - AI 資訊