TechByteByByte

Building an MCP Client

The other half of every connection. Connect to the real server you just built, discover its capabilities, call its tool, read its resource, and use its prompt — all from the client side.

#MCP#Client#FastMCP

Recall Module 9’s own closing promise — you built a real, working server. Every capability it exposes is genuinely useless until something actually connects to it. This module builds that other half.

Connecting to a real, existing server

Let’s connect directly to Module 9’s own, final server file.

We’ll open a real, live connection to the server, using the exact same FastMCP framework, this time from the client side.

import asyncio
from fastmcp import Client

async def main():
    async with Client("support_server.py") as client:  # launches and connects to the real server
        print("Connected.")  # the real, live connection is now open

asyncio.run(main())

Notice async with — recall your own async coursework’s real discipline; this genuinely ensures the connection is properly, safely closed once the block finishes, even if something inside it fails.

It’s worth being precise about what Client(...) genuinely accepts, since this course will use more than one real form. A local .py file path, like the one above, triggers a real stdio connection — launching the file as a genuine, local subprocess. A real URL, like Client("https://api.example.com/mcp"), connects over Streamable HTTP instead — recall Module 8’s own real transport distinction, inferred automatically from what you actually pass in.

Discovering what the server actually exposes

Recall Module 5’s own real tools/list — let’s call it for real this time, not just inspect its raw message shape.

async def main():
    async with Client("support_server.py") as client:
        tools = await client.list_tools()  # a real, live discovery call
        for tool in tools:
            print(f"{tool.name}: {tool.description}")

asyncio.run(main())

Run this against Module 9’s own server, and you’ll see the real, actual tool — get_order_status — along with its genuine, real description, discovered live, not hardcoded anywhere in this client’s own code.

Calling a real tool

Let’s actually use the discovered capability, not just list it.

We’ll call the real, connected server’s tool directly, and inspect its genuine, real result.

async def main():
    async with Client("support_server.py") as client:
        result = await client.call_tool("get_order_status", {"order_id": "O1"})  # a real, live tool call
        print(result)

asyncio.run(main())

Notice this genuinely triggers real, actual execution on the Server — the same ORDERS.get(...) lookup from Module 9’s own code, running live, with its real result sent back over the real connection.

Reading a real resource

Recall Module 6’s own precise distinction — let’s read real, existing context, not perform an action.

async def main():
    async with Client("support_server.py") as client:
        policy = await client.read_resource("policy://returns")  # a real, live resource read
        print(policy)

asyncio.run(main())

Using a real prompt

Recall Module 7’s own reusable templates — let’s actually request one.

async def main():
    async with Client("support_server.py") as client:
        prompt = await client.get_prompt("draft_response", {"order_id": "O1", "status": "Shipped"})
        print(prompt)  # the real, tuned prompt text, ready to send to an LLM

asyncio.run(main())

A complete, real client, tying everything together

Let’s build the genuine, full picture — a client that discovers, then uses, every real capability the server exposes.

flowchart TD
    A[Connect] --> B[list_tools / list_resources / list_prompts]
    B --> C{What's actually available?}
    C --> D[call_tool]
    C --> E[read_resource]
    C --> F[get_prompt]
import asyncio
from fastmcp import Client

async def main():
    async with Client("support_server.py") as client:
        # discover everything real this server actually exposes
        tools = await client.list_tools()
        resources = await client.list_resources()
        prompts = await client.list_prompts()
        print(f"Found {len(tools)} tools, {len(resources)} resources, {len(prompts)} prompts.")

        # actually use each real capability type
        status = await client.call_tool("get_order_status", {"order_id": "O1"})
        policy = await client.read_resource("policy://returns")
        response_prompt = await client.get_prompt("draft_response", {"order_id": "O1", "status": "Shipped"})

        print(status)
        print(policy)
        print(response_prompt)

asyncio.run(main())

This is genuinely the complete, real shape every Host application builds on — discover, then use, exactly the same real pattern regardless of how many capabilities a given Server actually exposes.

Common mistakes worth avoiding

Hardcoding tool names without ever calling list_tools() first. Recall this module’s own real discovery example — a genuinely robust client checks what’s actually available before assuming a specific capability exists, especially against a Server you don’t control.

Forgetting await on every real client call. Recall your LlamaIndex course’s own async-first warning — every one of these real calls is a genuine coroutine; omitting await hands you an unresolved object, not an actual result.

Not using async with, and leaving a real connection open. Recall this module’s own real example — the context manager genuinely guarantees cleanup, even when something inside the block fails; a bare, un-managed connection risks leaking real, open resources.

What you should take away from this module

  • A real MCP client connects, then genuinely discovers what a server exposes, before actually using any of it.
  • list_tools(), list_resources(), and list_prompts() are the real, live discovery calls; call_tool(), read_resource(), and get_prompt() are how you actually use what was discovered.
  • async with Client(...) is the real, correct pattern — guaranteeing a connection closes properly, even on failure.

Where this goes next

The next module connects everything to what you already know deeply: Connecting Agents to MCP — wiring a real, live MCP connection directly into an agent from your LangChain, LangGraph, or LlamaIndex coursework.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed