{"slug": "from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt", "title": "From Software Engineer to AI Engineer - Part 5: Scaling your tool belt", "summary": "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.", "body_md": "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.\n\nAn **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.\n\nNote 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.\n\nOnce you developed your tools, building an MCP server around it is actually pretty straightforward. Here we will expose our tools `calculate_refund_cost`\n\nand `search_payments_knowledge_base`\n\nover MCP. Create `app/mcp_server.py`\n\n:\n\n``` python\nfrom langchain_mcp_adapters.tools import to_fastmcp\nfrom mcp.server.fastmcp import FastMCP\n\nfrom app.tools import calculate_refund_cost, issue_refund\nfrom app.rag import search_payments_knowledge_base\n\nmcp = FastMCP(\n    \"payiq-tools\",\n    tools=[\n        to_fastmcp(calculate_refund_cost),\n        to_fastmcp(search_payments_knowledge_base),\n    ],\n)\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\")\n```\n\nStart the server with `python -m app.mcp_server`\n\nand lets curl that catalog to try to out. First, initialize the mcp session to get the session id:\n\n``` bash\n$ curl -sS http://localhost:8000/mcp \\\n    -X POST \\\n    -H \"Content-Type: application/json\" \\\n    -H \"Accept: application/json, text/event-stream\" \\\n    -D - \\\n    -o /dev/null \\\n    -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"curl\",\"version\":\"1.0\"}}}' |\n  awk -F': ' 'tolower($1) == \"mcp-session-id\" {print $2}' |\n  tr -d '\\r'\n\n208a02151828414894c1b0b0c33e3c2a\n```\n\nAnd then use it to get the catalog:\n\n``` bash\n$ curl http://localhost:8000/mcp \\\n  -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -H \"Mcp-Session-Id: 208a02151828414894c1b0b0c33e3c2a\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}' \\\n  | sed -n 's/^data: //p' | jq .\n\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 2,\n  \"result\": {\n    \"tools\": [\n      {\n        \"name\": \"calculate_refund_cost\",\n        \"description\": \"Calculate what [...] processing fees.\",\n        \"inputSchema\": {\n          \"description\": \"Calculate what refunding [...] the processing fees.\",\n          \"properties\": {\n            \"original_charge_eur\": {\n              \"title\": \"Original Charge Eur\",\n              \"type\": \"number\"\n            },\n            \"refund_amount_eur\": {\n              \"title\": \"Refund Amount Eur\",\n              \"type\": \"number\"\n            },\n            \"processing_fee_pct\": {\n              \"title\": \"Processing Fee Pct\",\n              \"type\": \"number\"\n            },\n            \"processing_fee_fixed_eur\": {\n              \"title\": \"Processing Fee Fixed Eur\",\n              \"type\": \"number\"\n            },\n            \"refund_admin_fee_eur\": {\n              \"default\": 0.25,\n              \"title\": \"Refund Admin Fee Eur\",\n              \"type\": \"number\"\n            }\n          },\n          \"required\": [\n            \"original_charge_eur\",\n            \"refund_amount_eur\",\n            \"processing_fee_pct\",\n            \"processing_fee_fixed_eur\"\n          ],\n          \"title\": \"calculate_refund_cost\",\n          \"type\": \"object\"\n        }\n      },\n      {\n        \"name\": \"search_payments_knowledge_base\",\n        \"description\": \"Search internal [...]ine\\\".\",\n        \"inputSchema\": {\n          \"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\\\".\",\n          \"properties\": {\n            \"query\": {\n              \"title\": \"Query\",\n              \"type\": \"string\"\n            }\n          },\n          \"required\": [\n            \"query\"\n          ],\n          \"title\": \"search_payments_knowledge_base\",\n          \"type\": \"object\"\n        }\n      }\n    ]\n  }\n}\n```\n\nExactly 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.\n\nNow we want to our application to use this tool catalog. Create `05_tool_mcp.py`\n\n(tip: compare it to Part 3):\n\n``` python\nimport asyncio\n\nfrom dotenv import load_dotenv\nfrom langchain.chat_models import init_chat_model\nfrom langchain_mcp_adapters.client import MultiServerMCPClient\n\nload_dotenv()\n\nasync def main():\n    client = MultiServerMCPClient(\n        {\n            \"payiq\": {\n                \"transport\": \"streamable_http\",\n                \"url\": \"http://localhost:8000/mcp\",\n            }\n        }\n    )\n    tools = await client.get_tools()\n    tool_map = {t.name: t for t in tools}\n\n    model = init_chat_model(\"anthropic:claude-sonnet-5\")\n    model_with_tools = model.bind_tools(tools)\n\n    question = (\n        \"Customer paid €480 on a European consumer card. \"\n        \"What does it cost us and how much costs a refund?\"\n    )\n    msg = await model_with_tools.ainvoke(question)\n    print(msg.tool_calls)\n\n    tool_messages = []\n    for tool_call in msg.tool_calls:\n        tool_messages.append(\n            await tool_map[tool_call[\"name\"]].ainvoke(tool_call)\n        )\n\n    next_msg = await model_with_tools.ainvoke(\n        [{\"role\": \"user\", \"content\": question}, msg, *tool_messages]\n    )\n    print(next_msg.tool_calls)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen I ran this snippet, the model first requested the `search_payments_knowledge_base`\n\ntool and then decided that it also wanted to use `calculate_refund_cost`\n\n:\n\n``` bash\n$ python 05_tool_mcp.py\n\n[{'name': 'search_payments_knowledge_base', 'args': {'query': 'European consumer card processing fees'}, 'id': 'toolu_01QWJJYSSySs2r9KLpMTNPgA', 'type': 'tool_call'}]\n\n[{'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'}]\n```\n\nAfter 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!\n\nFind all code samples in the companion repo here:\n\n[https://github.com/BjornvdLaan/ai-engineering-articles-code-samples]", "url": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt", "canonical_source": "https://dev.to/bjornvdlaan/from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt-2ejn", "published_at": "2026-09-01 22:04:00+00:00", "updated_at": "2026-09-01 22:23:31.937050+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure", "ai-agents"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt", "markdown": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt.md", "text": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt.txt", "jsonld": "https://wpnews.pro/news/from-software-engineer-to-ai-engineer-part-5-scaling-your-tool-belt.jsonld"}}