{"slug": "two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api", "title": "Two Protocols, Three Translations: How a Host Bridges MCP and an LLM Provider API", "summary": "A developer has published a walkthrough of how an MCP Host bridges the Model Context Protocol and an LLM provider API, tracing a single tool call through 20 steps across four phases. The account frames the architecture as \"two protocols, three translations, one complete tool-call lifecycle,\" showing how the Host discovers tools from an MCP server, converts them into the provider's tool-definition format, translates the model's tool decision back into an MCP request, and converts the result for the model's final answer.", "body_md": "**Following one question through the MCP stack — from the user's prompt to the final answer**\n\nModern AI applications increasingly sit between two different worlds: **MCP (Model Context Protocol)** and an **LLM provider API**.\n\nMCP provides a standardized way for an AI application to discover and invoke external tools.\n\nThe LLM provider API provides the interface through which the application sends prompts, tool definitions, and tool results to the model.\n\nThe important part is that these two systems do not necessarily speak the same format.\n\nThe **Host** sits between them.\n\nThe 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.\n\nThis creates a useful mental model:\n\n**Two protocols. Three translations. One complete tool-call lifecycle.**\n\nIn this article, we will follow one simple question through the entire process:\n\n**\"What is 5 plus 7?\"**\n\nOur MCP Server exposes one tool:\n\n``` php\nadd_numbers(a: number, b: number) -> number\n```\n\nWe will follow the request through **20 steps**, divided into four phases:\n\nBefore looking at the 20 steps, we need to understand the components involved.\n\nThe architecture looks approximately like this:\n\n```\n                    ┌──────────────────┐\n                    │       USER       │\n                    │ \"What is 5 + 7?\" │\n                    └────────┬─────────┘\n                             │\n                             ▼\n                    ┌──────────────────┐\n                    │       HOST       │\n                    │                  │\n                    │ Agent /          │\n                    │ Orchestrator     │\n                    └────────┬─────────┘\n                             │\n                        MCP / JSON-RPC\n                             │\n                             ▼\n                    ┌──────────────────┐\n                    │      CLIENT      │\n                    └────────┬─────────┘\n                             │\n                             ▼\n                    ┌──────────────────┐\n                    │      SERVER      │\n                    │                  │\n                    │ Tool Registry    │\n                    │                  │\n                    │ add_numbers      │\n                    └──────────────────┘\n\n                    ┌──────────────────┐\n                    │       HOST       │\n                    └────────┬─────────┘\n                             │\n                       Provider API\n                             │\n                             ▼\n                    ┌──────────────────┐\n                    │       LLM        │\n                    │                  │\n                    │ Tool selection   │\n                    │ Argument creation│\n                    └──────────────────┘\n```\n\nThe most important thing to understand is that there are two protocol boundaries.\n\nThe first is the **MCP side**.\n\nThe MCP Client and MCP Server communicate using MCP messages based on JSON-RPC.\n\nFor example:\n\n```\n{\n  \"method\": \"tools/list\",\n  \"id\": 1\n}\n```\n\nOr:\n\n```\n{\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"add_numbers\",\n    \"arguments\": {\n      \"a\": 5,\n      \"b\": 7\n    }\n  },\n  \"id\": 2\n}\n```\n\nThe second side is the **LLM provider API**.\n\nThe Host does not simply send those MCP JSON-RPC messages directly to the LLM.\n\nInstead, the Host translates the MCP tool definition into the format expected by the provider.\n\nFor example, conceptually:\n\n```\n{\n  \"name\": \"add_numbers\",\n  \"description\": \"Adds two numbers\",\n  \"parameters\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"a\": {\n        \"type\": \"number\"\n      },\n      \"b\": {\n        \"type\": \"number\"\n      }\n    },\n    \"required\": [\n      \"a\",\n      \"b\"\n    ]\n  }\n}\n```\n\nThe LLM can then respond with a provider-specific tool or function call such as:\n\n```\n{\n  \"function_call\": {\n    \"name\": \"add_numbers\",\n    \"arguments\": \"{\\\"a\\\":5,\\\"b\\\":7}\"\n  }\n}\n```\n\nThe Host then translates this back into an MCP `tools/call` request.\n\nThat is the bridge.\n\nThe first phase answers one fundamental question:\n\n**What tools are available?**\n\nAt this point, the LLM has not yet been asked to solve the user's question.\n\nThe Host first needs to discover what tools are available from the MCP Server.\n\nThe user enters:\n\n```\nWhat is 5 plus 7?\n```\n\nThe Host receives this prompt.\n\nAt this moment, the Host knows what the user wants, but it may not yet know what tools are available on the connected MCP Server.\n\nThe Host's orchestration layer starts the agent process.\n\nConceptually, we could imagine something like:\n\n```\nawait agent.run(\"What is 5 plus 7?\");\n```\n\nThe Host now needs to discover the tools that are available.\n\nIt asks the MCP Client to perform tool discovery.\n\nThe MCP Client sends an MCP request to the Server.\n\nConceptually:\n\n```\n{\n  \"method\": \"tools/list\",\n  \"id\": 1\n}\n```\n\nThis is an MCP request using JSON-RPC.\n\nThe important point is that this message is part of the MCP communication between the Client and Server.\n\nThe LLM is not involved yet.\n\nThe MCP Server receives the `tools/list` request.\n\nThe server maintains a tool registry.\n\nConceptually, the registry might contain:\n\n```\n{\n  name: \"add_numbers\",\n  description: \"Adds two numbers\",\n  inputSchema: {\n    ...\n  },\n  handler: ...\n}\n```\n\nThe registry contains both the public definition of the tool and the executable handler.\n\nHowever, the Server does not send the implementation of the handler to the Client.\n\nInstead, it exposes the public contract:\n\n```\nname\ndescription\ninput schema\n{\n  \"name\": \"add_numbers\",\n  \"description\": \"Adds two numbers\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"a\": {\n        \"type\": \"number\"\n      },\n      \"b\": {\n        \"type\": \"number\"\n      }\n    },\n    \"required\": [\n      \"a\",\n      \"b\"\n    ]\n  }\n}\n```\n\nThe Server is effectively saying:\n\n\"I have a tool called `add_numbers`. It accepts two numbers: `a` and `b`.\"\n\nThe Server responds to the Client.\n\n```\n{\n  \"id\": 1,\n  \"result\": {\n    \"tools\": [\n      {\n        \"name\": \"add_numbers\",\n        \"description\": \"Adds two numbers\",\n        \"inputSchema\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"a\": {\n              \"type\": \"number\"\n            },\n            \"b\": {\n              \"type\": \"number\"\n            }\n          },\n          \"required\": [\n            \"a\",\n            \"b\"\n          ]\n        }\n      }\n    ]\n  }\n}\n```\n\nNotice what was returned.\n\nThe Server returned:\n\n```\nname\ndescription\nschema\n```\n\nIt did not return:\n\n```\nthe implementation of the function\n```\n\nThe handler remains on the Server.\n\nThe MCP Client receives the response.\n\nIt passes the discovered tools back to the Host.\n\nThe Host now knows:\n\n```\nTool:\n    add_numbers\n\nArguments:\n    a: number\n    b: number\n```\n\nThe discovery phase is complete.\n\nThe Host can now tell the LLM what tools are available.\n\nThis is where one of the most important architectural concepts appears.\n\nThe Host has an MCP tool definition.\n\nBut the LLM provider does not necessarily understand MCP's tool representation.\n\nTherefore, the Host must perform a translation.\n\nThe Host converts the MCP tool schema into the tool/function schema expected by the LLM provider.\n\n```\nMCP Tool Schema\n       |\n       | Translation #1\n       v\nLLM Provider Tool Schema\n```\n\nThe MCP tool might look conceptually like:\n\n```\n{\n  \"name\": \"add_numbers\",\n  \"description\": \"Adds two numbers\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"a\": {\n        \"type\": \"number\"\n      },\n      \"b\": {\n        \"type\": \"number\"\n      }\n    },\n    \"required\": [\n      \"a\",\n      \"b\"\n    ]\n  }\n}\n```\n\nThe Host transforms it into something the provider understands:\n\n```\n{\n  \"name\": \"add_numbers\",\n  \"description\": \"Adds two numbers\",\n  \"parameters\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"a\": {\n        \"type\": \"number\"\n      },\n      \"b\": {\n        \"type\": \"number\"\n      }\n    },\n    \"required\": [\n      \"a\",\n      \"b\"\n    ]\n  }\n}\n```\n\nThe exact field names depend on the provider.\n\nThe important concept is the translation.\n\nThe Host is acting as an adapter between MCP and the provider API.\n\nThe Host now has two pieces of information:\n\n```\nUser prompt:\n\"What is 5 plus 7?\"\n\nAvailable tool:\nadd_numbers\n```\n\nThe Host sends both to the LLM provider.\n\n```\n{\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"What is 5 plus 7?\"\n    }\n  ],\n  \"tools\": [\n    {\n      \"name\": \"add_numbers\",\n      \"description\": \"Adds two numbers\",\n      \"parameters\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"a\": {\n            \"type\": \"number\"\n          },\n          \"b\": {\n            \"type\": \"number\"\n          }\n        },\n        \"required\": [\n          \"a\",\n          \"b\"\n        ]\n      }\n    }\n  ]\n}\n```\n\nThis is now a **provider API request**.\n\nIt is no longer the MCP `tools/list` request.\n\nThis distinction is critical.\n\nThe Host has crossed from:\n\n```\nMCP\n```\n\ninto:\n\n```\nLLM Provider API\n```\n\nThe LLM receives the user question and the available tool.\n\nIt reasons that the `add_numbers` tool is appropriate.\n\nIt generates the arguments:\n\n```\n{\n  \"a\": 5,\n  \"b\": 7\n}\n```\n\nThis is an important distinction.\n\nThe Server provided the schema.\n\nThe LLM generated the actual values.\n\nThe Server effectively said:\n\n```\na must be a number\nb must be a number\n```\n\nThe LLM generates:\n\n```\na = 5\nb = 7\n```\n\nThe model has therefore created new structured data that matches the schema it was given.\n\nThe provider returns something conceptually like:\n\n```\n{\n  \"function_call\": {\n    \"name\": \"add_numbers\",\n    \"arguments\": \"{\\\"a\\\":5,\\\"b\\\":7}\"\n  }\n}\n```\n\nAn extremely important point is that the LLM has **not executed the function**.\n\nIt has only requested that the Host execute it.\n\nThe LLM is effectively saying:\n\n\"I believe the `add_numbers` tool should be called with `a = 5` and `b = 7`.\"\n\nThe Host is still responsible for the actual invocation.\n\nNow the Host has the LLM's tool decision.\n\nBut the decision is expressed in the provider's format.\n\nThe Host needs to turn it back into an MCP request.\n\nThe Host performs the second translation.\n\nIt takes the provider's tool call:\n\n```\n{\n  \"name\": \"add_numbers\",\n  \"arguments\": \"{\\\"a\\\":5,\\\"b\\\":7}\"\n}\n```\n\nand converts it into an MCP `tools/call` request:\n\n```\n{\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"add_numbers\",\n    \"arguments\": {\n      \"a\": 5,\n      \"b\": 7\n    }\n  },\n  \"id\": 2\n}\n```\n\nThe Host has now crossed the protocol boundary again:\n\n```\nLLM Provider API\n       |\n       | Translation #2\n       v\nMCP / JSON-RPC\n```\n\nThe MCP Client sends the new request to the Server:\n\n```\n{\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"add_numbers\",\n    \"arguments\": {\n      \"a\": 5,\n      \"b\": 7\n    }\n  },\n  \"id\": 2\n}\n```\n\nNotice that this is a new JSON-RPC request.\n\nThe discovery request used:\n\n```\nid: 1\n```\n\nThe tool invocation uses:\n\n```\nid: 2\n```\n\nThe two requests are separate operations.\n\nThe Server receives the request.\n\nIt reads:\n\n```\nparams.name\n```\n\nwhich contains:\n\n```\nadd_numbers\n```\n\nThe Server looks up that name in its registry.\n\n``` js\nconst tool = registry.get(\"add_numbers\");\n```\n\nThe Server now has access to the tool definition and its handler.\n\nBefore executing anything, the Server validates the arguments.\n\nThe arguments are:\n\n```\n{\n  \"a\": 5,\n  \"b\": 7\n}\n```\n\nThe Server validates them against the tool's schema.\n\n```\nz.object({\n  a: z.number(),\n  b: z.number()\n});\n```\n\nThe validation succeeds:\n\n```\na = 5    ✓ number\n\nb = 7    ✓ number\n```\n\nThis step is extremely important.\n\nThe LLM generated the arguments.\n\nBut the MCP Server should still validate them.\n\nThe model should not be treated as a trusted source of executable input.\n\nThe schema acts as a contract between the tool definition and the actual execution.\n\nOnly now does the actual computation happen.\n\nThe Server executes:\n\n```\n5 + 7 = 12\n```\n\nThe handler might look like:\n\n```\nasync function addNumbers(args: {\n  a: number;\n  b: number;\n}) {\n  return args.a + args.b;\n}\n```\n\nThe Server then packages the result into an MCP-compatible result:\n\n```\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"Result: 12\"\n    }\n  ]\n}\n```\n\nThis is the first point in the lifecycle where the actual tool computation happens.\n\nEverything before this point was discovery, translation, decision-making, validation, or orchestration.\n\nThe Server sends the result back to the Client:\n\n```\n{\n  \"id\": 2,\n  \"result\": {\n    \"content\": [\n      {\n        \"type\": \"text\",\n        \"text\": \"Result: 12\"\n      }\n    ]\n  }\n}\n```\n\nThis is again MCP/JSON-RPC.\n\nThe result has not yet been given back to the LLM.\n\nThe Client receives it first.\n\nThe tool has now executed successfully.\n\nThe result is:\n\n```\nResult: 12\n```\n\nBut the user did not ask to see raw tool output.\n\nThe user asked a natural-language question.\n\nThe Host therefore needs the LLM to turn the structured tool result into a final response.\n\nThe MCP Client receives:\n\n```\nResult: 12\n```\n\nand passes the result back to the Host.\n\nThe Host now has the output of the MCP tool.\n\nThe Host performs the third and final translation.\n\nIt converts:\n\n```\nMCP Tool Result\nLLM Provider Tool Result\nMCP Result\n    |\n    | Translation #3\n    v\nProvider Tool Output\n{\n  \"role\": \"tool\",\n  \"name\": \"add_numbers\",\n  \"content\": \"Result: 12\"\n}\n```\n\nThe exact structure depends on the LLM provider.\n\nThe important concept is that the Host again acts as the bridge.\n\nThe LLM now receives the conversation context together with the tool result.\n\n```\nUser:\n\nWhat is 5 plus 7?\n\nTool:\n\nResult: 12\n```\n\nThe LLM can now generate a natural-language response:\n\n```\n5 plus 7 is 12.\n```\n\nThe tool provided the computation.\n\nThe LLM provides the conversational response.\n\nFinally, the Host returns:\n\n```\n5 plus 7 is 12.\n```\n\nThe user sees:\n\n**5 plus 7 is 12.**\n\nThe complete lifecycle is finished.\n\nThe easiest way to understand the entire architecture is to focus on the three translations.\n\nThe Host converts:\n\n```\nMCP Tool Schema\n        ↓\nLLM Provider Tool Schema\n```\n\nThis allows the LLM to understand which tools are available and what arguments they accept.\n\n```\nLLM Provider Tool Call\n        ↓\nMCP tools/call\n```\n\nThis takes the LLM's decision and turns it into an actual MCP tool invocation.\n\n```\nMCP Tool Result\n        ↓\nLLM Provider Tool Result\n```\n\nThis allows the LLM to see the result of the tool execution and formulate the final response.\n\nWe can now compress the entire architecture into one diagram:\n\n```\nUSER\n  |\n  | \"What is 5 + 7?\"\n  v\nHOST\n  |\n  | Start discovery\n  v\nCLIENT\n  |\n  | tools/list\n  v\nSERVER\n  |\n  | Read tool registry\n  | Return tool definitions\n  v\nCLIENT\n  |\n  v\nHOST\n  |\n  | Translation #1\n  | MCP schema -> Provider schema\n  v\nLLM\n  |\n  | Decide:\n  | add_numbers(a=5,b=7)\n  v\nHOST\n  |\n  | Translation #2\n  | Provider call -> MCP tools/call\n  v\nCLIENT\n  |\n  | tools/call\n  v\nSERVER\n  |\n  | Find tool\n  | Validate arguments\n  | Execute handler\n  |\n  | 5 + 7 = 12\n  v\nCLIENT\n  |\n  v\nHOST\n  |\n  | Translation #3\n  | MCP result -> Provider result\n  v\nLLM\n  |\n  | \"5 plus 7 is 12.\"\n  v\nHOST\n  |\n  v\nUSER\n```\n\nHere is the entire process in one place.\n\n**1. User**\n\nThe user asks:\n\n```\n\"What is 5 plus 7?\"\n```\n\n**2. Host**\n\nThe Host starts the agent and initiates tool discovery.\n\n**3. Client**\n\nThe MCP Client sends:\n\n```\n{\n  \"method\": \"tools/list\",\n  \"id\": 1\n}\n```\n\n**4. Server**\n\nThe MCP Server reads its tool registry and prepares the available tool definitions.\n\n**5. Server**\n\nThe Server responds:\n\n```\n{\n  \"id\": 1,\n  \"result\": {\n    \"tools\": [\n      {\n        \"name\": \"add_numbers\",\n        \"description\": \"Adds two numbers\",\n        \"inputSchema\": {}\n      }\n    ]\n  }\n}\n```\n\n**6. Client**\n\nThe Client passes the tool definitions back to the Host.\n\n**7. Host — Translation #1**\n\n```\nMCP tool schema\n        ↓\nProvider tool schema\n```\n\n**8. Host**\n\nThe Host sends the user's prompt and the translated tools to the LLM provider.\n\n**9. LLM**\n\nThe LLM determines that `add_numbers` is appropriate and generates:\n\n```\n{\n  \"a\": 5,\n  \"b\": 7\n}\n```\n\n**10. LLM**\n\nThe LLM returns its provider-specific tool/function call.\n\n**11. Host — Translation #2**\n\nThe Host converts the provider tool call into an MCP `tools/call`.\n\n**12. Client**\n\nThe Client sends:\n\n```\n{\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"add_numbers\",\n    \"arguments\": {\n      \"a\": 5,\n      \"b\": 7\n    }\n  },\n  \"id\": 2\n}\n```\n\n**13. Server**\n\nThe Server looks up `add_numbers` in its registry.\n\n**14. Server**\n\nThe Server validates:\n\n```\na = 5 ✓\nb = 7 ✓\n```\n\n**15. Server**\n\nThe Server executes the handler:\n\n```\n5 + 7 = 12\n```\n\n**16. Server**\n\nThe Server returns the MCP result.\n\n**17. Client**\n\nThe Client receives the MCP result and passes it to the Host.\n\n**18. Host — Translation #3**\n\n```\nMCP result\n    ↓\nProvider tool result\n```\n\nand sends it to the LLM.\n\n**19. LLM**\n\n```\n\"5 plus 7 is 12.\"\n```\n\n**20. User**\n\n```\n5 plus 7 is 12.\n```\n\nThe following simplified TypeScript code demonstrates the main architectural boundaries.\n\n``` js\nimport { z } from \"zod\";\n\n// ==========================================\n// 1. TOOL DEFINITION\n// ==========================================\n\nconst addNumbersSchema = z.object({\n  a: z.number(),\n  b: z.number(),\n});\n\ntype AddNumbersArgs = z.infer<typeof addNumbersSchema>;\n\nconst tool = {\n  name: \"add_numbers\",\n\n  description: \"Adds two numbers\",\n\n  inputSchema: {\n    type: \"object\",\n    properties: {\n      a: { type: \"number\" },\n      b: { type: \"number\" },\n    },\n    required: [\"a\", \"b\"],\n  },\n\n  handler: async (args: AddNumbersArgs) => {\n    return args.a + args.b;\n  },\n};\n\n// ==========================================\n// 2. MCP: tools/list\n// ==========================================\n\nfunction listTools() {\n  return {\n    tools: [\n      {\n        name: tool.name,\n        description: tool.description,\n        inputSchema: tool.inputSchema,\n      },\n    ],\n  };\n}\n\n// ==========================================\n// 3. TRANSLATION #1\n//\n// MCP schema\n//      ↓\n// Provider schema\n// ==========================================\n\nfunction toProviderTool(mcpTool: any) {\n  return {\n    name: mcpTool.name,\n    description: mcpTool.description,\n    parameters: mcpTool.inputSchema,\n  };\n}\n\n// ==========================================\n// 4. TRANSLATION #2\n//\n// Provider function call\n//      ↓\n// MCP tools/call\n// ==========================================\n\nfunction toMcpToolCall(functionCall: any) {\n  return {\n    method: \"tools/call\",\n\n    params: {\n      name: functionCall.name,\n\n      arguments: JSON.parse(\n        functionCall.arguments\n      ),\n    },\n\n    id: 2,\n  };\n}\n\n// ==========================================\n// 5. SERVER EXECUTION\n// ==========================================\n\nasync function executeTool(\n  name: string,\n  arguments_: unknown\n) {\n\n  if (name !== tool.name) {\n    throw new Error(\n      `Unknown tool: ${name}`\n    );\n  }\n\n  // Server-side validation\n  const args =\n    addNumbersSchema.parse(arguments_);\n\n  // Actual computation\n  const result =\n    await tool.handler(args);\n\n  return {\n    content: [\n      {\n        type: \"text\",\n        text: `Result: ${result}`,\n      },\n    ],\n  };\n}\n\n// ==========================================\n// 6. TRANSLATION #3\n//\n// MCP result\n//      ↓\n// Provider tool result\n// ==========================================\n\nfunction toProviderToolResult(\n  mcpResult: any\n) {\n  return {\n    role: \"tool\",\n    content: mcpResult.content,\n  };\n}\n```\n\nThe same architecture can be represented in Python.\n\n``` python\nfrom typing import Any\nimport json\n\n# ==========================================\n# 1. TOOL DEFINITION\n# ==========================================\n\ndef add_numbers(a: float, b: float) -> float:\n    return a + b\n\nTOOL = {\n    \"name\": \"add_numbers\",\n\n    \"description\": \"Adds two numbers\",\n\n    \"inputSchema\": {\n        \"type\": \"object\",\n\n        \"properties\": {\n            \"a\": {\n                \"type\": \"number\"\n            },\n            \"b\": {\n                \"type\": \"number\"\n            }\n        },\n\n        \"required\": [\n            \"a\",\n            \"b\"\n        ]\n    }\n}\n\n# ==========================================\n# 2. MCP: tools/list\n# ==========================================\n\ndef list_tools() -> dict:\n    return {\n        \"tools\": [\n            {\n                \"name\": TOOL[\"name\"],\n                \"description\": TOOL[\"description\"],\n                \"inputSchema\": TOOL[\"inputSchema\"]\n            }\n        ]\n    }\n\n# ==========================================\n# 3. TRANSLATION #1\n#\n# MCP schema\n#      ↓\n# Provider schema\n# ==========================================\n\ndef to_provider_tool(\n    mcp_tool: dict\n) -> dict:\n\n    return {\n        \"name\": mcp_tool[\"name\"],\n        \"description\": mcp_tool[\"description\"],\n        \"parameters\": mcp_tool[\"inputSchema\"]\n    }\n\n# ==========================================\n# 4. TRANSLATION #2\n#\n# Provider function call\n#      ↓\n# MCP tools/call\n# ==========================================\n\ndef to_mcp_call(\n    function_call: dict\n) -> dict:\n\n    arguments = json.loads(\n        function_call[\"arguments\"]\n    )\n\n    return {\n        \"method\": \"tools/call\",\n\n        \"params\": {\n            \"name\": function_call[\"name\"],\n            \"arguments\": arguments\n        },\n\n        \"id\": 2\n    }\n\n# ==========================================\n# 5. SERVER EXECUTION\n# ==========================================\n\ndef execute_tool(\n    name: str,\n    arguments: dict\n) -> dict:\n\n    if name != \"add_numbers\":\n        raise ValueError(\n            f\"Unknown tool: {name}\"\n        )\n\n    # Server-side validation\n\n    if not isinstance(\n        arguments.get(\"a\"),\n        (int, float)\n    ):\n        raise ValueError(\n            \"a must be a number\"\n        )\n\n    if not isinstance(\n        arguments.get(\"b\"),\n        (int, float)\n    ):\n        raise ValueError(\n            \"b must be a number\"\n        )\n\n    # Actual computation\n\n    result = add_numbers(\n        arguments[\"a\"],\n        arguments[\"b\"]\n    )\n\n    return {\n        \"content\": [\n            {\n                \"type\": \"text\",\n                \"text\": f\"Result: {result}\"\n            }\n        ]\n    }\n\n# ==========================================\n# 6. TRANSLATION #3\n#\n# MCP result\n#      ↓\n# Provider tool result\n# ==========================================\n\ndef to_provider_result(\n    mcp_result: dict\n) -> dict:\n\n    return {\n        \"role\": \"tool\",\n        \"content\": mcp_result[\"content\"]\n    }\n```\n\nWe can combine the concepts into a simplified Host implementation.\n\n```\nasync function runAgent(\n  userPrompt: string\n) {\n\n  // ======================================\n  // PHASE 1: DISCOVERY\n  // ======================================\n\n  const mcpResponse =\n    await mcpClient.request({\n      method: \"tools/list\",\n      id: 1,\n    });\n\n  const mcpTools =\n    mcpResponse.result.tools;\n\n  // ======================================\n  // TRANSLATION #1\n  // ======================================\n\n  const providerTools =\n    mcpTools.map(\n      toProviderTool\n    );\n\n  // ======================================\n  // PHASE 2: ASK THE LLM\n  // ======================================\n\n  const llmResponse =\n    await llmProvider.chat({\n\n      messages: [\n        {\n          role: \"user\",\n          content: userPrompt,\n        },\n      ],\n\n      tools: providerTools,\n    });\n\n  // ======================================\n  // LLM SELECTED A TOOL\n  // ======================================\n\n  const functionCall =\n    llmResponse.function_call;\n\n  // ======================================\n  // TRANSLATION #2\n  // ======================================\n\n  const mcpCall =\n    toMcpToolCall(\n      functionCall\n    );\n\n  // ======================================\n  // PHASE 3: EXECUTE THROUGH MCP\n  // ======================================\n\n  const toolResult =\n    await mcpClient.request(\n      mcpCall\n    );\n\n  // ======================================\n  // TRANSLATION #3\n  // ======================================\n\n  const providerResult =\n    toProviderToolResult(\n      toolResult\n    );\n\n  // ======================================\n  // FINAL LLM RESPONSE\n  // ======================================\n\n  const finalResponse =\n    await llmProvider.chat({\n\n      messages: [\n\n        {\n          role: \"user\",\n          content: userPrompt,\n        },\n\n        {\n          role: \"tool\",\n          content:\n            providerResult.content,\n        },\n\n      ],\n    });\n\n  return finalResponse;\n}\n```\n\nUnderstanding this flow changes how we think about MCP.\n\nMCP is not simply:\n\n\"A way for an LLM to call a function.\"\n\nInstead, MCP provides a standardized interface through which an AI application can discover and invoke tools.\n\nThe Host connects that standardized interface to an LLM provider.\n\nThis creates a separation of responsibilities.\n\nThe MCP Server does not need to know which LLM is being used.\n\nThe LLM does not need to know how the MCP Server internally implements its tools.\n\nThe Host coordinates everything.\n\nThe architecture can therefore look like:\n\n```\n                  ┌─────────────────┐\n                  │   MCP SERVER    │\n                  │                 │\n                  │  add_numbers    │\n                  │  search         │\n                  │  database       │\n                  │  APIs           │\n                  └────────┬────────┘\n                           │\n                          MCP\n                           │\n                           ▼\n                  ┌─────────────────┐\n                  │      HOST       │\n                  │                 │\n                  │  Discovery      │\n                  │  Translation    │\n                  │  Orchestration  │\n                  └────────┬────────┘\n                           │\n              ┌────────────┼────────────┐\n              │            │            │\n              ▼            ▼            ▼\n            LLM A        LLM B        LLM C\n```\n\nThe MCP Server exposes tools.\n\nThe Host translates and orchestrates.\n\nThe LLM decides which tool to use.\n\nThe Server validates and executes.\n\nThe LLM finally turns the result into natural language.\n\nThis is probably the most important takeaway from this entire flow.\n\nIt is tempting to think about the architecture like this:\n\n```\nUser\n  ↓\nMCP\n  ↓\nLLM\n```\n\nBut a more accurate representation is:\n\n```\n                    ┌─────────────┐\n                    │    USER     │\n                    └──────┬──────┘\n                           │\n                           ▼\n                    ┌─────────────┐\n                    │    HOST     │\n                    └──────┬──────┘\n                           │\n             ┌─────────────┴─────────────┐\n             │                           │\n             ▼                           ▼\n        MCP Protocol              Provider API\n             │                           │\n             ▼                           ▼\n          CLIENT                        LLM\n             │\n             ▼\n          SERVER\n             │\n             ▼\n           TOOLS\n```\n\nThe Host is therefore an **adapter and orchestrator** between the two protocol worlds.\n\nAnother important distinction is responsibility.\n\nThe MCP Server says:\n\n\"Here is a tool I expose, its description, and the arguments it accepts.\"\n\nThe LLM decides:\n\n\"This tool is appropriate for the user's question, and I want to call it with these arguments.\"\n\nThe Host coordinates the process.\n\nThis can be summarized as:\n\n| Component | Responsibility | \n|---|---|\n| User | Provides intent | \n| Host | Orchestrates the lifecycle | \n| MCP Client | Communicates with MCP Servers | \n| MCP Server | Exposes and executes tools | \n| Tool Registry | Stores tool definitions and handlers | \n| LLM | Decides whether and how to use a tool | \n| Schema | Defines valid arguments | \n| Tool Handler | Performs the actual operation | \n\nOne of the most important details in this architecture is validation.\n\n```\n{\n  \"a\": 5,\n  \"b\": 7\n}\n```\n\nBut the Server should not simply assume those arguments are valid.\n\nThe Server validates them against its own schema.\n\n``` js\nconst schema = z.object({\n  a: z.number(),\n  b: z.number(),\n});\n\nconst validatedArgs =\n  schema.parse(arguments);\n```\n\nThis creates an important boundary:\n\n```\nLLM-generated data\n        |\n        v\nServer validation\n        |\n        | valid\n        v\nTool execution\n```\n\nThe LLM is probabilistic.\n\nThe tool execution environment should remain deterministic and controlled.\n\nThe schema therefore becomes an important contract between model-generated arguments and real-world execution.\n\nIf there is only one diagram to remember from this entire article, it should be this:\n\n```\nUSER\n  │\n  │ \"What is 5 + 7?\"\n  ▼\nHOST\n  │\n  │ MCP discovery\n  ▼\nCLIENT\n  │\n  │ tools/list\n  ▼\nSERVER\n  │\n  │ Tool registry\n  │ Tool definitions\n  ▼\nCLIENT\n  │\n  ▼\nHOST\n  │\n  │ Translation #1\n  │\n  │ MCP schema\n  │       ↓\n  │ Provider schema\n  ▼\nLLM\n  │\n  │ Tool decision\n  │ add_numbers\n  │ a = 5\n  │ b = 7\n  ▼\nHOST\n  │\n  │ Translation #2\n  │\n  │ Provider tool call\n  │       ↓\n  │ MCP tools/call\n  ▼\nCLIENT\n  │\n  │ tools/call\n  ▼\nSERVER\n  │\n  │ Find tool\n  │ Validate arguments\n  │ Execute handler\n  │\n  │ 5 + 7 = 12\n  ▼\nCLIENT\n  │\n  ▼\nHOST\n  │\n  │ Translation #3\n  │\n  │ MCP result\n  │       ↓\n  │ Provider tool result\n  ▼\nLLM\n  │\n  │ \"5 plus 7 is 12.\"\n  ▼\nHOST\n  │\n  ▼\nUSER\n```\n\nThe entire lifecycle can therefore be summarized in one sentence:\n\n**Discover → Translate → Decide → Translate → Execute → Translate → Respond.**\n\nOr, even more simply:\n\n**The Host is the bridge between MCP and the LLM provider.**\n\nMCP standardizes how the Host discovers and invokes tools.\n\nThe LLM provider API defines how the Host communicates with the model.\n\nThe Host connects these two worlds through **three translations**:\n\n```\n1. MCP Tool Schema\n        ↓\n   Provider Tool Schema\n\n2. Provider Tool Call\n        ↓\n   MCP tools/call\n\n3. MCP Tool Result\n        ↓\n   Provider Tool Result\n```\n\nAnd that is the real story behind a seemingly simple question such as:\n\nBehind 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.**\n\nThat is the MCP tool-call lifecycle.\n\n**Two protocols. Three translations. One complete request.**", "url": "https://wpnews.pro/news/two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api", "canonical_source": "https://dev.to/ignacio_gonzalezbohorque/two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api-4pkh", "published_at": "2026-10-05 17:43:25+00:00", "updated_at": "2026-10-05 17:47:49.808561+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "large-language-models", "ai-tools", "developer-tools"], "entities": ["Model Context Protocol", "MCP", "JSON-RPC"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api", "markdown": "https://wpnews.pro/news/two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api.md", "text": "https://wpnews.pro/news/two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api.txt", "jsonld": "https://wpnews.pro/news/two-protocols-three-translations-how-a-host-bridges-mcp-and-an-llm-provider-api.jsonld"}}