DEV Community

Cover image for Two Protocols, Three Translations: How a Host Bridges MCP and an LLM Provider API
Ignacio Gonzalez Bohorquez
Ignacio Gonzalez Bohorquez

Posted on

Two Protocols, Three Translations: How a Host Bridges MCP and an LLM Provider API

Following one question through the MCP stack — from the user's prompt to the final answer

Modern AI applications increasingly sit between two different worlds: MCP (Model Context Protocol) and an LLM provider API.

MCP provides a standardized way for an AI application to discover and invoke external tools.

The LLM provider API provides the interface through which the application sends prompts, tool definitions, and tool results to the model.

The important part is that these two systems do not necessarily speak the same format.

The Host sits between them.

The Host discovers tools from an MCP Server, translates those tools into the format expected by the LLM provider, receives the LLM's tool decision, translates that decision back into an MCP request, executes the tool, and finally translates the result back into the provider's format so the LLM can produce a natural-language response.

This creates a useful mental model:

Two protocols. Three translations. One complete tool-call lifecycle.

In this article, we will follow one simple question through the entire process:

"What is 5 plus 7?"

Our MCP Server exposes one tool:

add_numbers(a: number, b: number) -> number
Enter fullscreen mode Exit fullscreen mode

We will follow the request through 20 steps, divided into four phases:

  1. Discovery — Steps 1–6
  2. Translation and LLM Decision — Steps 7–10
  3. Execution — Steps 11–16
  4. Result and Response — Steps 17–20

1. The Architecture

Before looking at the 20 steps, we need to understand the components involved.

The architecture looks approximately like this:

                    ┌──────────────────┐
                    │       USER       │
                    │ "What is 5 + 7?" │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │       HOST       │
                    │                  │
                    │ Agent /          │
                    │ Orchestrator     │
                    └────────┬─────────┘
                             │
                        MCP / JSON-RPC
                             │
                             ▼
                    ┌──────────────────┐
                    │      CLIENT      │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │      SERVER      │
                    │                  │
                    │ Tool Registry    │
                    │                  │
                    │ add_numbers      │
                    └──────────────────┘


                    ┌──────────────────┐
                    │       HOST       │
                    └────────┬─────────┘
                             │
                       Provider API
                             │
                             ▼
                    ┌──────────────────┐
                    │       LLM        │
                    │                  │
                    │ Tool selection   │
                    │ Argument creation│
                    └──────────────────┘
Enter fullscreen mode Exit fullscreen mode

The most important thing to understand is that there are two protocol boundaries.

The first is the MCP side.

The MCP Client and MCP Server communicate using MCP messages based on JSON-RPC.

For example:

{
  "method": "tools/list",
  "id": 1
}
Enter fullscreen mode Exit fullscreen mode

Or:

{
  "method": "tools/call",
  "params": {
    "name": "add_numbers",
    "arguments": {
      "a": 5,
      "b": 7
    }
  },
  "id": 2
}
Enter fullscreen mode Exit fullscreen mode

The second side is the LLM provider API.

The Host does not simply send those MCP JSON-RPC messages directly to the LLM.

Instead, the Host translates the MCP tool definition into the format expected by the provider.

For example, conceptually:

{
  "name": "add_numbers",
  "description": "Adds two numbers",
  "parameters": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number"
      },
      "b": {
        "type": "number"
      }
    },
    "required": [
      "a",
      "b"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The LLM can then respond with a provider-specific tool or function call such as:

{
  "function_call": {
    "name": "add_numbers",
    "arguments": "{\"a\":5,\"b\":7}"
  }
}
Enter fullscreen mode Exit fullscreen mode

The Host then translates this back into an MCP tools/call request.

That is the bridge.


Phase 1 — Discovery

Steps 1–6

The first phase answers one fundamental question:

What tools are available?

At this point, the LLM has not yet been asked to solve the user's question.

The Host first needs to discover what tools are available from the MCP Server.


Step 1 — The User asks a question

The user enters:

What is 5 plus 7?
Enter fullscreen mode Exit fullscreen mode

The Host receives this prompt.

At this moment, the Host knows what the user wants, but it may not yet know what tools are available on the connected MCP Server.


Step 2 — The Host starts discovery

The Host's orchestration layer starts the agent process.

Conceptually, we could imagine something like:

await agent.run("What is 5 plus 7?");
Enter fullscreen mode Exit fullscreen mode

The Host now needs to discover the tools that are available.

It asks the MCP Client to perform tool discovery.


Step 3 — The Client sends tools/list

The MCP Client sends an MCP request to the Server.

Conceptually:

{
  "method": "tools/list",
  "id": 1
}
Enter fullscreen mode Exit fullscreen mode

This is an MCP request using JSON-RPC.

The important point is that this message is part of the MCP communication between the Client and Server.

The LLM is not involved yet.


Step 4 — The Server reads its tool registry

The MCP Server receives the tools/list request.

The server maintains a tool registry.

Conceptually, the registry might contain:

{
  name: "add_numbers",
  description: "Adds two numbers",
  inputSchema: {
    ...
  },
  handler: ...
}
Enter fullscreen mode Exit fullscreen mode

The registry contains both the public definition of the tool and the executable handler.

However, the Server does not send the implementation of the handler to the Client.

Instead, it exposes the public contract:

name
description
input schema
Enter fullscreen mode Exit fullscreen mode

For example:

{
  "name": "add_numbers",
  "description": "Adds two numbers",
  "inputSchema": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number"
      },
      "b": {
        "type": "number"
      }
    },
    "required": [
      "a",
      "b"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The Server is effectively saying:

"I have a tool called add_numbers. It accepts two numbers: a and b."


Step 5 — The Server returns the tool list

The Server responds to the Client.

Conceptually:

{
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "add_numbers",
        "description": "Adds two numbers",
        "inputSchema": {
          "type": "object",
          "properties": {
            "a": {
              "type": "number"
            },
            "b": {
              "type": "number"
            }
          },
          "required": [
            "a",
            "b"
          ]
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice what was returned.

The Server returned:

name
description
schema
Enter fullscreen mode Exit fullscreen mode

It did not return:

the implementation of the function
Enter fullscreen mode Exit fullscreen mode

The handler remains on the Server.


Step 6 — The Client gives the tools to the Host

The MCP Client receives the response.

It passes the discovered tools back to the Host.

The Host now knows:

Tool:
    add_numbers

Arguments:
    a: number
    b: number
Enter fullscreen mode Exit fullscreen mode

The discovery phase is complete.

The Host can now tell the LLM what tools are available.


Phase 2 — Translation and LLM Decision

Steps 7–10

This is where one of the most important architectural concepts appears.

The Host has an MCP tool definition.

But the LLM provider does not necessarily understand MCP's tool representation.

Therefore, the Host must perform a translation.


Step 7 — Translation #1

The Host converts the MCP tool schema into the tool/function schema expected by the LLM provider.

Conceptually:

MCP Tool Schema
       |
       | Translation #1
       v
LLM Provider Tool Schema
Enter fullscreen mode Exit fullscreen mode

The MCP tool might look conceptually like:

{
  "name": "add_numbers",
  "description": "Adds two numbers",
  "inputSchema": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number"
      },
      "b": {
        "type": "number"
      }
    },
    "required": [
      "a",
      "b"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The Host transforms it into something the provider understands:

{
  "name": "add_numbers",
  "description": "Adds two numbers",
  "parameters": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number"
      },
      "b": {
        "type": "number"
      }
    },
    "required": [
      "a",
      "b"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The exact field names depend on the provider.

The important concept is the translation.

The Host is acting as an adapter between MCP and the provider API.


Step 8 — The Host sends the prompt and tools to the LLM

The Host now has two pieces of information:

User prompt:
"What is 5 plus 7?"

Available tool:
add_numbers
Enter fullscreen mode Exit fullscreen mode

The Host sends both to the LLM provider.

Conceptually:

{
  "messages": [
    {
      "role": "user",
      "content": "What is 5 plus 7?"
    }
  ],
  "tools": [
    {
      "name": "add_numbers",
      "description": "Adds two numbers",
      "parameters": {
        "type": "object",
        "properties": {
          "a": {
            "type": "number"
          },
          "b": {
            "type": "number"
          }
        },
        "required": [
          "a",
          "b"
        ]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This is now a provider API request.

It is no longer the MCP tools/list request.

This distinction is critical.

The Host has crossed from:

MCP
Enter fullscreen mode Exit fullscreen mode

into:

LLM Provider API
Enter fullscreen mode Exit fullscreen mode

Step 9 — The LLM decides which tool to use

The LLM receives the user question and the available tool.

It reasons that the add_numbers tool is appropriate.

It generates the arguments:

{
  "a": 5,
  "b": 7
}
Enter fullscreen mode Exit fullscreen mode

This is an important distinction.

The Server provided the schema.

The LLM generated the actual values.

The Server effectively said:

a must be a number
b must be a number
Enter fullscreen mode Exit fullscreen mode

The LLM generates:

a = 5
b = 7
Enter fullscreen mode Exit fullscreen mode

The model has therefore created new structured data that matches the schema it was given.


Step 10 — The LLM returns its tool decision

The provider returns something conceptually like:

{
  "function_call": {
    "name": "add_numbers",
    "arguments": "{\"a\":5,\"b\":7}"
  }
}
Enter fullscreen mode Exit fullscreen mode

An extremely important point is that the LLM has not executed the function.

It has only requested that the Host execute it.

The LLM is effectively saying:

"I believe the add_numbers tool should be called with a = 5 and b = 7."

The Host is still responsible for the actual invocation.


Phase 3 — Execution

Steps 11–16

Now the Host has the LLM's tool decision.

But the decision is expressed in the provider's format.

The Host needs to turn it back into an MCP request.


Step 11 — Translation #2

The Host performs the second translation.

It takes the provider's tool call:

{
  "name": "add_numbers",
  "arguments": "{\"a\":5,\"b\":7}"
}
Enter fullscreen mode Exit fullscreen mode

and converts it into an MCP tools/call request:

{
  "method": "tools/call",
  "params": {
    "name": "add_numbers",
    "arguments": {
      "a": 5,
      "b": 7
    }
  },
  "id": 2
}
Enter fullscreen mode Exit fullscreen mode

The Host has now crossed the protocol boundary again:

LLM Provider API
       |
       | Translation #2
       v
MCP / JSON-RPC
Enter fullscreen mode Exit fullscreen mode

Step 12 — The Client sends tools/call

The MCP Client sends the new request to the Server:

{
  "method": "tools/call",
  "params": {
    "name": "add_numbers",
    "arguments": {
      "a": 5,
      "b": 7
    }
  },
  "id": 2
}
Enter fullscreen mode Exit fullscreen mode

Notice that this is a new JSON-RPC request.

The discovery request used:

id: 1
Enter fullscreen mode Exit fullscreen mode

The tool invocation uses:

id: 2
Enter fullscreen mode Exit fullscreen mode

The two requests are separate operations.


Step 13 — The Server looks up the tool

The Server receives the request.

It reads:

params.name
Enter fullscreen mode Exit fullscreen mode

which contains:

add_numbers
Enter fullscreen mode Exit fullscreen mode

The Server looks up that name in its registry.

Conceptually:

const tool = registry.get("add_numbers");
Enter fullscreen mode Exit fullscreen mode

The Server now has access to the tool definition and its handler.


Step 14 — The Server validates the arguments

Before executing anything, the Server validates the arguments.

The arguments are:

{
  "a": 5,
  "b": 7
}
Enter fullscreen mode Exit fullscreen mode

The Server validates them against the tool's schema.

Conceptually:

z.object({
  a: z.number(),
  b: z.number()
});
Enter fullscreen mode Exit fullscreen mode

The validation succeeds:

a = 5    ✓ number

b = 7    ✓ number
Enter fullscreen mode Exit fullscreen mode

This step is extremely important.

The LLM generated the arguments.

But the MCP Server should still validate them.

The model should not be treated as a trusted source of executable input.

The schema acts as a contract between the tool definition and the actual execution.


Step 15 — The Server executes the handler

Only now does the actual computation happen.

The Server executes:

5 + 7 = 12
Enter fullscreen mode Exit fullscreen mode

The handler might look like:

async function addNumbers(args: {
  a: number;
  b: number;
}) {
  return args.a + args.b;
}
Enter fullscreen mode Exit fullscreen mode

The Server then packages the result into an MCP-compatible result:

{
  "content": [
    {
      "type": "text",
      "text": "Result: 12"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This is the first point in the lifecycle where the actual tool computation happens.

Everything before this point was discovery, translation, decision-making, validation, or orchestration.


Step 16 — The Server returns the result

The Server sends the result back to the Client:

{
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Result: 12"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

This is again MCP/JSON-RPC.

The result has not yet been given back to the LLM.

The Client receives it first.


Phase 4 — Result and Response

Steps 17–20

The tool has now executed successfully.

The result is:

Result: 12
Enter fullscreen mode Exit fullscreen mode

But the user did not ask to see raw tool output.

The user asked a natural-language question.

The Host therefore needs the LLM to turn the structured tool result into a final response.


Step 17 — The Client receives the result

The MCP Client receives:

Result: 12
Enter fullscreen mode Exit fullscreen mode

and passes the result back to the Host.

The Host now has the output of the MCP tool.


Step 18 — Translation #3

The Host performs the third and final translation.

It converts:

MCP Tool Result
Enter fullscreen mode Exit fullscreen mode

into:

LLM Provider Tool Result
Enter fullscreen mode Exit fullscreen mode

Conceptually:

MCP Result
    |
    | Translation #3
    v
Provider Tool Output
Enter fullscreen mode Exit fullscreen mode

For example:

{
  "role": "tool",
  "name": "add_numbers",
  "content": "Result: 12"
}
Enter fullscreen mode Exit fullscreen mode

The exact structure depends on the LLM provider.

The important concept is that the Host again acts as the bridge.


Step 19 — The LLM generates the final answer

The LLM now receives the conversation context together with the tool result.

Conceptually:

User:

What is 5 plus 7?


Tool:

Result: 12
Enter fullscreen mode Exit fullscreen mode

The LLM can now generate a natural-language response:

5 plus 7 is 12.
Enter fullscreen mode Exit fullscreen mode

The tool provided the computation.

The LLM provides the conversational response.


Step 20 — The User sees the final answer

Finally, the Host returns:

5 plus 7 is 12.
Enter fullscreen mode Exit fullscreen mode

The user sees:

5 plus 7 is 12.

The complete lifecycle is finished.


The Three Translations

The easiest way to understand the entire architecture is to focus on the three translations.

Translation #1

The Host converts:

MCP Tool Schema
        ↓
LLM Provider Tool Schema
Enter fullscreen mode Exit fullscreen mode

This allows the LLM to understand which tools are available and what arguments they accept.


Translation #2

The Host converts:

LLM Provider Tool Call
        ↓
MCP tools/call
Enter fullscreen mode Exit fullscreen mode

This takes the LLM's decision and turns it into an actual MCP tool invocation.


Translation #3

The Host converts:

MCP Tool Result
        ↓
LLM Provider Tool Result
Enter fullscreen mode Exit fullscreen mode

This allows the LLM to see the result of the tool execution and formulate the final response.


The Complete Flow

We can now compress the entire architecture into one diagram:

USER
  |
  | "What is 5 + 7?"
  v
HOST
  |
  | Start discovery
  v
CLIENT
  |
  | tools/list
  v
SERVER
  |
  | Read tool registry
  | Return tool definitions
  v
CLIENT
  |
  v
HOST
  |
  | Translation #1
  | MCP schema -> Provider schema
  v
LLM
  |
  | Decide:
  | add_numbers(a=5,b=7)
  v
HOST
  |
  | Translation #2
  | Provider call -> MCP tools/call
  v
CLIENT
  |
  | tools/call
  v
SERVER
  |
  | Find tool
  | Validate arguments
  | Execute handler
  |
  | 5 + 7 = 12
  v
CLIENT
  |
  v
HOST
  |
  | Translation #3
  | MCP result -> Provider result
  v
LLM
  |
  | "5 plus 7 is 12."
  v
HOST
  |
  v
USER
Enter fullscreen mode Exit fullscreen mode

The Complete 20-Step Lifecycle

Here is the entire process in one place.

Phase 1 — Discovery

1. User

The user asks:

"What is 5 plus 7?"
Enter fullscreen mode Exit fullscreen mode

2. Host

The Host starts the agent and initiates tool discovery.

3. Client

The MCP Client sends:

{
  "method": "tools/list",
  "id": 1
}
Enter fullscreen mode Exit fullscreen mode

4. Server

The MCP Server reads its tool registry and prepares the available tool definitions.

5. Server

The Server responds:

{
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "add_numbers",
        "description": "Adds two numbers",
        "inputSchema": {}
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Client

The Client passes the tool definitions back to the Host.


Phase 2 — Translation and LLM Decision

7. Host — Translation #1

The Host converts:

MCP tool schema
        ↓
Provider tool schema
Enter fullscreen mode Exit fullscreen mode

8. Host

The Host sends the user's prompt and the translated tools to the LLM provider.

9. LLM

The LLM determines that add_numbers is appropriate and generates:

{
  "a": 5,
  "b": 7
}
Enter fullscreen mode Exit fullscreen mode

10. LLM

The LLM returns its provider-specific tool/function call.


Phase 3 — Execution

11. Host — Translation #2

The Host converts the provider tool call into an MCP tools/call.

12. Client

The Client sends:

{
  "method": "tools/call",
  "params": {
    "name": "add_numbers",
    "arguments": {
      "a": 5,
      "b": 7
    }
  },
  "id": 2
}
Enter fullscreen mode Exit fullscreen mode

13. Server

The Server looks up add_numbers in its registry.

14. Server

The Server validates:

a = 5 ✓
b = 7 ✓
Enter fullscreen mode Exit fullscreen mode

15. Server

The Server executes the handler:

5 + 7 = 12
Enter fullscreen mode Exit fullscreen mode

16. Server

The Server returns the MCP result.


Phase 4 — Result and Response

17. Client

The Client receives the MCP result and passes it to the Host.

18. Host — Translation #3

The Host converts:

MCP result
    ↓
Provider tool result
Enter fullscreen mode Exit fullscreen mode

and sends it to the LLM.

19. LLM

The LLM generates:

"5 plus 7 is 12."
Enter fullscreen mode Exit fullscreen mode

20. User

The user sees:

5 plus 7 is 12.
Enter fullscreen mode Exit fullscreen mode

TypeScript Example

The following simplified TypeScript code demonstrates the main architectural boundaries.

import { z } from "zod";

// ==========================================
// 1. TOOL DEFINITION
// ==========================================

const addNumbersSchema = z.object({
  a: z.number(),
  b: z.number(),
});

type AddNumbersArgs = z.infer<typeof addNumbersSchema>;

const tool = {
  name: "add_numbers",

  description: "Adds two numbers",

  inputSchema: {
    type: "object",
    properties: {
      a: { type: "number" },
      b: { type: "number" },
    },
    required: ["a", "b"],
  },

  handler: async (args: AddNumbersArgs) => {
    return args.a + args.b;
  },
};


// ==========================================
// 2. MCP: tools/list
// ==========================================

function listTools() {
  return {
    tools: [
      {
        name: tool.name,
        description: tool.description,
        inputSchema: tool.inputSchema,
      },
    ],
  };
}


// ==========================================
// 3. TRANSLATION #1
//
// MCP schema
//      ↓
// Provider schema
// ==========================================

function toProviderTool(mcpTool: any) {
  return {
    name: mcpTool.name,
    description: mcpTool.description,
    parameters: mcpTool.inputSchema,
  };
}


// ==========================================
// 4. TRANSLATION #2
//
// Provider function call
//      ↓
// MCP tools/call
// ==========================================

function toMcpToolCall(functionCall: any) {
  return {
    method: "tools/call",

    params: {
      name: functionCall.name,

      arguments: JSON.parse(
        functionCall.arguments
      ),
    },

    id: 2,
  };
}


// ==========================================
// 5. SERVER EXECUTION
// ==========================================

async function executeTool(
  name: string,
  arguments_: unknown
) {

  if (name !== tool.name) {
    throw new Error(
      `Unknown tool: ${name}`
    );
  }

  // Server-side validation
  const args =
    addNumbersSchema.parse(arguments_);

  // Actual computation
  const result =
    await tool.handler(args);

  return {
    content: [
      {
        type: "text",
        text: `Result: ${result}`,
      },
    ],
  };
}


// ==========================================
// 6. TRANSLATION #3
//
// MCP result
//      ↓
// Provider tool result
// ==========================================

function toProviderToolResult(
  mcpResult: any
) {
  return {
    role: "tool",
    content: mcpResult.content,
  };
}
Enter fullscreen mode Exit fullscreen mode

Python Example

The same architecture can be represented in Python.

from typing import Any
import json


# ==========================================
# 1. TOOL DEFINITION
# ==========================================

def add_numbers(a: float, b: float) -> float:
    return a + b


TOOL = {
    "name": "add_numbers",

    "description": "Adds two numbers",

    "inputSchema": {
        "type": "object",

        "properties": {
            "a": {
                "type": "number"
            },
            "b": {
                "type": "number"
            }
        },

        "required": [
            "a",
            "b"
        ]
    }
}


# ==========================================
# 2. MCP: tools/list
# ==========================================

def list_tools() -> dict:
    return {
        "tools": [
            {
                "name": TOOL["name"],
                "description": TOOL["description"],
                "inputSchema": TOOL["inputSchema"]
            }
        ]
    }


# ==========================================
# 3. TRANSLATION #1
#
# MCP schema
#      ↓
# Provider schema
# ==========================================

def to_provider_tool(
    mcp_tool: dict
) -> dict:

    return {
        "name": mcp_tool["name"],
        "description": mcp_tool["description"],
        "parameters": mcp_tool["inputSchema"]
    }


# ==========================================
# 4. TRANSLATION #2
#
# Provider function call
#      ↓
# MCP tools/call
# ==========================================

def to_mcp_call(
    function_call: dict
) -> dict:

    arguments = json.loads(
        function_call["arguments"]
    )

    return {
        "method": "tools/call",

        "params": {
            "name": function_call["name"],
            "arguments": arguments
        },

        "id": 2
    }


# ==========================================
# 5. SERVER EXECUTION
# ==========================================

def execute_tool(
    name: str,
    arguments: dict
) -> dict:

    if name != "add_numbers":
        raise ValueError(
            f"Unknown tool: {name}"
        )

    # Server-side validation

    if not isinstance(
        arguments.get("a"),
        (int, float)
    ):
        raise ValueError(
            "a must be a number"
        )

    if not isinstance(
        arguments.get("b"),
        (int, float)
    ):
        raise ValueError(
            "b must be a number"
        )

    # Actual computation

    result = add_numbers(
        arguments["a"],
        arguments["b"]
    )

    return {
        "content": [
            {
                "type": "text",
                "text": f"Result: {result}"
            }
        ]
    }


# ==========================================
# 6. TRANSLATION #3
#
# MCP result
#      ↓
# Provider tool result
# ==========================================

def to_provider_result(
    mcp_result: dict
) -> dict:

    return {
        "role": "tool",
        "content": mcp_result["content"]
    }
Enter fullscreen mode Exit fullscreen mode

A Simplified End-to-End Host

We can combine the concepts into a simplified Host implementation.

async function runAgent(
  userPrompt: string
) {

  // ======================================
  // PHASE 1: DISCOVERY
  // ======================================

  const mcpResponse =
    await mcpClient.request({
      method: "tools/list",
      id: 1,
    });

  const mcpTools =
    mcpResponse.result.tools;


  // ======================================
  // TRANSLATION #1
  // ======================================

  const providerTools =
    mcpTools.map(
      toProviderTool
    );


  // ======================================
  // PHASE 2: ASK THE LLM
  // ======================================

  const llmResponse =
    await llmProvider.chat({

      messages: [
        {
          role: "user",
          content: userPrompt,
        },
      ],

      tools: providerTools,
    });


  // ======================================
  // LLM SELECTED A TOOL
  // ======================================

  const functionCall =
    llmResponse.function_call;


  // ======================================
  // TRANSLATION #2
  // ======================================

  const mcpCall =
    toMcpToolCall(
      functionCall
    );


  // ======================================
  // PHASE 3: EXECUTE THROUGH MCP
  // ======================================

  const toolResult =
    await mcpClient.request(
      mcpCall
    );


  // ======================================
  // TRANSLATION #3
  // ======================================

  const providerResult =
    toProviderToolResult(
      toolResult
    );


  // ======================================
  // FINAL LLM RESPONSE
  // ======================================

  const finalResponse =
    await llmProvider.chat({

      messages: [

        {
          role: "user",
          content: userPrompt,
        },

        {
          role: "tool",
          content:
            providerResult.content,
        },

      ],
    });


  return finalResponse;
}
Enter fullscreen mode Exit fullscreen mode

Why This Architecture Matters

Understanding this flow changes how we think about MCP.

MCP is not simply:

"A way for an LLM to call a function."

Instead, MCP provides a standardized interface through which an AI application can discover and invoke tools.

The Host connects that standardized interface to an LLM provider.

This creates a separation of responsibilities.

The MCP Server does not need to know which LLM is being used.

The LLM does not need to know how the MCP Server internally implements its tools.

The Host coordinates everything.

The architecture can therefore look like:

                  ┌─────────────────┐
                  │   MCP SERVER    │
                  │                 │
                  │  add_numbers    │
                  │  search         │
                  │  database       │
                  │  APIs           │
                  └────────┬────────┘
                           │
                          MCP
                           │
                           ▼
                  ┌─────────────────┐
                  │      HOST       │
                  │                 │
                  │  Discovery      │
                  │  Translation    │
                  │  Orchestration  │
                  └────────┬────────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
            LLM A        LLM B        LLM C
Enter fullscreen mode Exit fullscreen mode

The MCP Server exposes tools.

The Host translates and orchestrates.

The LLM decides which tool to use.

The Server validates and executes.

The LLM finally turns the result into natural language.


MCP Is Not the LLM Provider API

This is probably the most important takeaway from this entire flow.

It is tempting to think about the architecture like this:

User
  ↓
MCP
  ↓
LLM
Enter fullscreen mode Exit fullscreen mode

But a more accurate representation is:

                    ┌─────────────┐
                    │    USER     │
                    └──────┬──────┘
                           │
                           ▼
                    ┌─────────────┐
                    │    HOST     │
                    └──────┬──────┘
                           │
             ┌─────────────┴─────────────┐
             │                           │
             ▼                           ▼
        MCP Protocol              Provider API
             │                           │
             ▼                           ▼
          CLIENT                        LLM
             │
             ▼
          SERVER
             │
             ▼
           TOOLS
Enter fullscreen mode Exit fullscreen mode

The Host is therefore an adapter and orchestrator between the two protocol worlds.


The MCP Server Does Not Tell the LLM What to Do

Another important distinction is responsibility.

The MCP Server says:

"Here is a tool I expose, its description, and the arguments it accepts."

The LLM decides:

"This tool is appropriate for the user's question, and I want to call it with these arguments."

The Host coordinates the process.

The Server validates and executes.

This can be summarized as:

Component Responsibility
User Provides intent
Host Orchestrates the lifecycle
MCP Client Communicates with MCP Servers
MCP Server Exposes and executes tools
Tool Registry Stores tool definitions and handlers
LLM Decides whether and how to use a tool
Schema Defines valid arguments
Tool Handler Performs the actual operation

The Most Important Security Boundary

One of the most important details in this architecture is validation.

The LLM generates:

{
  "a": 5,
  "b": 7
}
Enter fullscreen mode Exit fullscreen mode

But the Server should not simply assume those arguments are valid.

The Server validates them against its own schema.

For example:

const schema = z.object({
  a: z.number(),
  b: z.number(),
});

const validatedArgs =
  schema.parse(arguments);
Enter fullscreen mode Exit fullscreen mode

This creates an important boundary:

LLM-generated data
        |
        v
Server validation
        |
        | valid
        v
Tool execution
Enter fullscreen mode Exit fullscreen mode

The LLM is probabilistic.

The tool execution environment should remain deterministic and controlled.

The schema therefore becomes an important contract between model-generated arguments and real-world execution.


The Final Mental Model

If there is only one diagram to remember from this entire article, it should be this:

USER
  │
  │ "What is 5 + 7?"
  ▼
HOST
  │
  │ MCP discovery
  ▼
CLIENT
  │
  │ tools/list
  ▼
SERVER
  │
  │ Tool registry
  │ Tool definitions
  ▼
CLIENT
  │
  ▼
HOST
  │
  │ Translation #1
  │
  │ MCP schema
  │       ↓
  │ Provider schema
  ▼
LLM
  │
  │ Tool decision
  │ add_numbers
  │ a = 5
  │ b = 7
  ▼
HOST
  │
  │ Translation #2
  │
  │ Provider tool call
  │       ↓
  │ MCP tools/call
  ▼
CLIENT
  │
  │ tools/call
  ▼
SERVER
  │
  │ Find tool
  │ Validate arguments
  │ Execute handler
  │
  │ 5 + 7 = 12
  ▼
CLIENT
  │
  ▼
HOST
  │
  │ Translation #3
  │
  │ MCP result
  │       ↓
  │ Provider tool result
  ▼
LLM
  │
  │ "5 plus 7 is 12."
  ▼
HOST
  │
  ▼
USER
Enter fullscreen mode Exit fullscreen mode

The entire lifecycle can therefore be summarized in one sentence:

Discover → Translate → Decide → Translate → Execute → Translate → Respond.

Or, even more simply:

The Host is the bridge between MCP and the LLM provider.

MCP standardizes how the Host discovers and invokes tools.

The LLM provider API defines how the Host communicates with the model.

The Host connects these two worlds through three translations:

1. MCP Tool Schema
        ↓
   Provider Tool Schema

2. Provider Tool Call
        ↓
   MCP tools/call

3. MCP Tool Result
        ↓
   Provider Tool Result
Enter fullscreen mode Exit fullscreen mode

And that is the real story behind a seemingly simple question such as:

"What is 5 plus 7?"

Behind that one sentence is an entire lifecycle of discovery, protocol messages, schema translation, model decision-making, argument generation, validation, tool execution, result translation, and final natural-language generation.

That is the MCP tool-call lifecycle.

Two protocols. Three translations. One complete request.

Top comments (0)