From Software Engineer to AI Engineer - Part 5: Scaling your tool belt A developer detailed how to scale AI tooling by exposing tool catalogs through the Model Context Protocol (MCP), using a Python example that wraps custom tools in an MCP server. The post explains the roles of MCP clients and servers, the JSON-RPC 2.0-based protocol, and demonstrates retrieving a tool catalog via curl. This approach allows AI applications to reuse tools across projects, similar to how software libraries are shared. We learned about tools and wrote them ourselves. This is cute, but an application writing all of its own tools is not scalable. In software development, we put functionality in libraries and frameworks and reuse it across projects. MCP the Model Context Protocol is all about exposing tool catalogs to models. An MCP server is maintained either by a SaaS provider or a company's internal AI platform team. An MCP server publishes a catalog of tools for other AI applications to use. As we saw in Part 3 https://dev.to/bjornvdlaan/from-software-engineer-to-ai-engineer-part-3-2o9k , each tool in the catalog has a name, typed parameters and a description. An AI application uses an MCP client to retrieve that catalog and hand it to a model. AI engineers develop both MCP clients and servers, similar to how a backend engineer builds APIs and calls other APIs as well. Communication happens via the MCP protocol , which is based on the JSON-RPC 2.0 protocol. These messages are sent over either streamable HTTP or stdio. With streamable HTTP, the server sits behind a URL, which suits shared and hosted deployments. Clients using stdio spawn the MCP server as a subprocess and talk over stdin and stdout. Note that the model never speaks MCP. That talking is done by the tools, which get the tool catalog from the MCP server and feed it back to the model. This catalog is similar to the local tool catalog from Part 3. And when the model requests a specific tool, it is a tool that sends a request to the MCP server. Are you still following? Let's look at an example. Once you developed your tools, building an MCP server around it is actually pretty straightforward. Here we will expose our tools calculate refund cost and search payments knowledge base over MCP. Create app/mcp server.py : python from langchain mcp adapters.tools import to fastmcp from mcp.server.fastmcp import FastMCP from app.tools import calculate refund cost, issue refund from app.rag import search payments knowledge base mcp = FastMCP "payiq-tools", tools= to fastmcp calculate refund cost , to fastmcp search payments knowledge base , , if name == " main ": mcp.run transport="streamable-http" Start the server with python -m app.mcp server and lets curl that catalog to try to out. First, initialize the mcp session to get the session id: bash $ curl -sS http://localhost:8000/mcp \ -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -D - \ -o /dev/null \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' | awk -F': ' 'tolower $1 == "mcp-session-id" {print $2}' | tr -d '\r' 208a02151828414894c1b0b0c33e3c2a And then use it to get the catalog: bash $ curl http://localhost:8000/mcp \ -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Mcp-Session-Id: 208a02151828414894c1b0b0c33e3c2a" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | sed -n 's/^data: //p' | jq . { "jsonrpc": "2.0", "id": 2, "result": { "tools": { "name": "calculate refund cost", "description": "Calculate what ... processing fees.", "inputSchema": { "description": "Calculate what refunding ... the processing fees.", "properties": { "original charge eur": { "title": "Original Charge Eur", "type": "number" }, "refund amount eur": { "title": "Refund Amount Eur", "type": "number" }, "processing fee pct": { "title": "Processing Fee Pct", "type": "number" }, "processing fee fixed eur": { "title": "Processing Fee Fixed Eur", "type": "number" }, "refund admin fee eur": { "default": 0.25, "title": "Refund Admin Fee Eur", "type": "number" } }, "required": "original charge eur", "refund amount eur", "processing fee pct", "processing fee fixed eur" , "title": "calculate refund cost", "type": "object" } }, { "name": "search payments knowledge base", "description": "Search internal ... ine\".", "inputSchema": { "description": "Search internal notes on processing fees, ... rather than guessing.\n\nArgs:\n query: What you need to know, e.g. \"card processing fees\" or\n \"chargeback response deadline\".", "properties": { "query": { "title": "Query", "type": "string" } }, "required": "query" , "title": "search payments knowledge base", "type": "object" } } } } Exactly the same structure as the tool catalog we saw in Part 3 Imagine that your company has a bunch of tools to access git repositories, ticket systems, monitoring tooling and more. All of these can be exposed to AI applications company-wide via such MCP servers. Now we want to our application to use this tool catalog. Create 05 tool mcp.py tip: compare it to Part 3 : python import asyncio from dotenv import load dotenv from langchain.chat models import init chat model from langchain mcp adapters.client import MultiServerMCPClient load dotenv async def main : client = MultiServerMCPClient { "payiq": { "transport": "streamable http", "url": "http://localhost:8000/mcp", } } tools = await client.get tools tool map = {t.name: t for t in tools} model = init chat model "anthropic:claude-sonnet-5" model with tools = model.bind tools tools question = "Customer paid €480 on a European consumer card. " "What does it cost us and how much costs a refund?" msg = await model with tools.ainvoke question print msg.tool calls tool messages = for tool call in msg.tool calls: tool messages.append await tool map tool call "name" .ainvoke tool call next msg = await model with tools.ainvoke {"role": "user", "content": question}, msg, tool messages print next msg.tool calls if name == " main ": asyncio.run main When I ran this snippet, the model first requested the search payments knowledge base tool and then decided that it also wanted to use calculate refund cost : bash $ python 05 tool mcp.py {'name': 'search payments knowledge base', 'args': {'query': 'European consumer card processing fees'}, 'id': 'toolu 01QWJJYSSySs2r9KLpMTNPgA', 'type': 'tool call'} {'name': 'calculate refund cost', 'args': {'original charge eur': 480, 'refund amount eur': 480, 'processing fee pct': 1.8, 'processing fee fixed eur': 0.25}, 'id': 'toolu 01Lwosv4XVE3QJTSpeCbj4xz', 'type': 'tool call'} After reading the snippet above, you probably screamed at the screen: "Keep looping on those tool calls until you get the final answer ". And you'd be right, we should do that. As we'll soon discover, that loop is what separates a model from an agent. Things are starting to feel pretty advanced, and the next article takes the final step to become AGENTIC Find all code samples in the companion repo here: https://github.com/BjornvdLaan/ai-engineering-articles-code-samples