cd /news/developer-tools/from-software-engineer-to-ai-enginee… · home topics developer-tools article
[ARTICLE · art-118315] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read4 min views1 publishedSep 1, 2026

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, 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

:

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:

$ 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:

$ 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):

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

:

$ 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]

── more in #developer-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/from-software-engine…] indexed:0 read:4min 2026-09-01 ·