Model Context Protocol: vznikající standard pro potřeby aplikací využívajících AI a LLM (2.část)

3. 4. 2025
Doba čtení: 25 minut

Sdílet

Dvě humanoidní postavy propojují svou mysl Autor: Root.cz s využitím Zoner AI
Na praktických příkladech si ukážeme zpracování a deserializace dat vrácených MCP serverem, zjistíme, zda dokáže server obsloužit více klientů souběžně a na závěr si popíšeme posílání rastrových dat serverem klientovi.

Obsah

1. Krátké zopakování: MCP server s definovaným nástrojem

2. MCP klient volající nástroj spouštěný z MCP serveru

3. Úplný zdrojový kód klienta, který získá hodnoty faktoriálů a vytiskne je

4. MCP server se zdrojem se seznamem knih autorů

5. MCP klient, který přečte zvolený zdroj a získá z něj strukturovaná data

6. Odlišný oddělovač selektorů

7. Chování MCP serveru po příchodu většího množství požadavků

8. Přístup ke „zpomalenému“ MCP serveru z MCP klienta

9. Souběžný přístup ke „zpomalenému“ MCP serveru

10. Asynchronní souběžný přístup ke „zpomalenému“ MCP serveru

11. Podpora rastrových obrázků v balíčku mcp

12. MCP server posílající rastrový obrázek

13. Dekódování obrázku na straně MCP klienta

14. Repositář s demonstračními příklady

15. Příloha: články o knihovně PIL/Pillow

16. Odkazy na Internetu

1. Krátké zopakování: MCP server s definovaným nástrojem

úvodním článku o MCP (Model Context Protocol) jsme se seznámili se základními vlastnosti tohoto protokolu. Ukázali jsme si taktéž knihovnu mcp určenou pro programovací jazyk Python, která umožňuje tvorbu MCP serverů i MCP klientů. A nezapomněli jsme ani na MCP Inspector, což je vývojářský nástroj zajišťující připojení k MCP serverům (nebo i k jejich spuštění) a interaktivní komunikaci se serverem.

Pro krátké zopakování si znovu ukažme zdrojový kód MCP serveru, ve kterém je definován jeden zdroj (resource) bez vyžadovaných selektorů, dále zdroj se selektorem (konkrétně se jménem klienta) a konečně i nástroj (tool), který na základě zadaného vstupu vypočítá hodnotu faktoriálu.

Poznámka: zdroj se selektorem či s více selektory se v terminologii MCP nazývá resource template.

Zdrojový kód serveru vypadá takto:

"""MCP server se zdrojem,  dynamickým zdrojem a definicí nástroje (tool)."""
 
from mcp.server.fastmcp import FastMCP
 
# konstrukce serveru
mcp = FastMCP("Test")
 
 
@mcp.tool()
def factorial(n: int) -> int:
    """Výpočet faktoriálu ve smyčce."""
    f = 1
    for x in range(1, n + 1):
        f *= x
    return f
 
 
@mcp.resource("pozdrav://")
def pozdrav1() -> str:
    """Odpověď s pozdravem."""
    return "Hello, dear client"
 
 
@mcp.resource("pozdrav://{name}")
def pozdrav2(name: str) -> str:
    """Odpověď s osobním pozdravem."""
    return f"Hello, {name}"
 
 
# přímé spuštění serveru v režimu SSE (Server-Sent Events)
if __name__ == "__main__":
    mcp.run(transport="sse")
Poznámka: tento server lze spustit přímo přes interpret jazyka Python; nemusíme tedy používat nástroj mcp.

2. MCP klient volající nástroj spouštěný z MCP serveru

Podívejme se nyní na to, jaká je základní struktura jednoduchého MCP klienta, který zvolený nástroj (tool) zavolá, předá mu všechny požadované argumenty, počká na přijaté výsledky a nakonec získá výslednou hodnotu poslanou klientem. Klient si sám zajistí spuštění MCP serveru a navázání komunikace s transportem STDIO:

# parametry pro spuštění MCP serveru
server_params = StdioServerParameters(
    command="python",  # spustí se tento příkaz
    args=["mcp_server_5.py"],  # a předají se mu následující parametry
    env=None,  # lze definovat i proměnné prostředí
)

Spuštění serveru (a na konci jeho ukončení) se děje v rámci správce kontextu (context manager):

async with stdio_client(server_params) as (read, write):
    ...
    ...
    ...

Nejprve získáme objekt představující sezení (session obsahující kontextové informace o navázaném připojení; sezení inicializujeme:

async with ClientSession(read, write) as session:
    await session.initialize()

Mimochodem, k objektu typu ClientSession si samozřejmě můžete zobrazit nápovědu přímo z REPLu Pythonu:

>>> from mcp import ClientSession, StdioServerParameters
 
>>> c=ClientSession(None, None)
 
>>> help(c)
 
class ClientSession(mcp.shared.session.BaseSession)
 |  ClientSession(read_stream: anyio.streams.memory.MemoryObjectReceiveStream[mcp.types.JSONRPCMessage | Exception], write_stream: anyio.streams.memory.MemoryObjectSendStream[mcp.types.JSONRPCMessage], read_timeout_seconds: datetime.timedelta | None = None, sampling_callback: mcp.client.session.SamplingFnT | None = None, list_roots_callback: mcp.client.session.ListRootsFnT | None = None, logging_callback: mcp.client.session.LoggingFnT | None = None) -> None
 |
 |  Method resolution order:
 |      ClientSession
 |      mcp.shared.session.BaseSession
 |      typing.Generic
 |      builtins.object
 |

Samotné volání nástroje, který je spouštěný přes MCP server, je v MCP klientovi provedeno asynchronně, ostatně jako všechna ostatní volání MCP serveru (zdroje atd.):

data = await session.call_tool("factorial")

Typicky je však nutné volanému nástroji předat argumenty, protože se vlastně nejedná o nic jiného, než o vzdálené volání kódu (RPC). Argumenty se specifikují v keyword parametru nazvaném arguments, jehož hodnotou je mapa obsahující jako klíče jména argumentů a hodnoty jsou (nepřekvapivě) předávané hodnoty. To je umožněno, protože hlavička metody call_tool vypadá následovně:

async call_tool(name: str, arguments: dict[str, typing.Any] | None = None)
    -> mcp.types.CallToolResult method of mcp.client.session.ClientSession instance
    Send a tools/call request.

V případě výpočtu faktoriálu tedy volání nástroje může vypadat následovně:

data = await session.call_tool("factorial", arguments={"n": n})

Můžeme se například pokusit o získání faktoriálů 0! až 10! (včetně) s následným výpisem získaných výsledků:

for n in range(11):
    # zavolání nástroje
    data = await session.call_tool("factorial", arguments={"n": n})
    factorial = data.content[0].text
    print(n, factorial)

3. Úplný zdrojový kód klienta, který získá hodnoty faktoriálů a vytiskne je

Úplný zdrojový kód MCP klienta popsaného v předchozí kapitole, který dokáže spustit MCP server, komunikovat s ním přes STDIO (standardní vstupy a výstupy) a získat hodnoty faktoriálů vypočtené nástrojem (tool) spouštěným z MCP serveru, vypadá ve své nejjednodušší podobě následovně:

"""MCP klient, který zavolá nástroj."""
 
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
 
# parametry pro spuštění MCP serveru
server_params = StdioServerParameters(
    command="python",  # spustí se tento příkaz
    args=["mcp_server_5.py"],  # a předají se mu následující parametry
    env=None,  # lze definovat i proměnné prostředí
)
 
 
async def run():
    """Realizace klienta."""
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
 
            for n in range(11):
                # zavolání nástroje
                data = await session.call_tool("factorial", arguments={"n": n})
                factorial = data.content[0].text
                print(n, factorial)
 
 
# přímé spuštění klienta
if __name__ == "__main__":
    import asyncio
 
    asyncio.run(run())
Poznámka: povšimněte si, jak snadné je přečtení odpovědi nástroje. Pouze z vráceného objektu přečteme atribut content (což je sekvence), vybereme první prvek a z něho atribut text.

Klienta běžným způsobem spustíme příkazem python jméno_skriptu.py a získáme potřebné výsledky. O inicializaci MCP serveru se již nemusíme starat, protože si klient server spustí automaticky. Výsledky by měly vypadat takto:

0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
8 40320
9 362880
10 3628800

V případě, že bychom ovšem vypisovali přímo odpovědi serveru, dostali bychom textovou podobu objektu, který je zpracován protokolem MCP a převeden do podoby Pythonovského objektu knihovnou mcp (to však většinou není zapotřebí):

meta=None content=[TextContent(type='text', text='1', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='1', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='2', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='6', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='24', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='120', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='720', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='5040', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='40320', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='362880', annotations=None)] isError=False
meta=None content=[TextContent(type='text', text='3628800', annotations=None)] isError=False

4. MCP server se zdrojem se seznamem knih autorů

V dnešním druhém demonstračním příkladu si připomeneme, jak by mohl vypadat jednoduchý MCP server, který pro zadaného autora (jméno, příjmení) vrací seznam knih. Jedná se o triviální implementaci bez databáze, takže se pro všechny autory vrací stejný seznam. To však pro základní testy nemusí být na škodu. Namísto nástroje (tool) v tomto případě použijeme zdroj. Ten je na straně serveru definován pod jménem author:// se selektorem name a surname, přičemž tyto selektory jsou odděleny pomlčkou (nemusíme tedy používat jen lomítka):

"""Jednoduchý MCP server s jediným definovaným dynamickým zdrojem."""
 
from mcp.server.fastmcp import FastMCP
 
# konstrukce serveru
mcp = FastMCP("Test")
 
 
@mcp.resource("author://{name}-{surname}")
def author(name: str, surname: str) -> list:
    """Knihy od vybraného autora."""
    return [
        {"name": name, "surname": surname, "title": "Foo", "year": 1900},
        {"name": name, "surname": surname, "title": "Bar", "year": 2005},
        {"name": name, "surname": surname, "title": "Baz", "year": 2025},
    ]

Tento server je možné spustit jak se STDIO transportem, tak i s SSE. Spustit ho můžeme nástrojem mcp nebo přímo z MCP klienta.

5. MCP klient, který přečte zvolený zdroj a získá z něj strukturovaná data

Nyní bude zajímavé zjistit, jak vlastně vypadá MCP klient, který dokáže přečíst a zpracovat jména knih napsaná vybraným autorem. Jediná zajímavá (resp. nová) část se týká zpracování dat, které nám knihovna mcp vrátí. Proto po spuštění klienta a přečtení odpovědi ze serveru zobrazíme jak „surová“ data, která byla přečtena a vrácena klientovi, tak i vlastní text (ano, čistý text!), s JSON odpovědí. O parsing JSONu se musíme postarat programově, což je zvláštní, ale současná verze knihovny mcp tuto vlastnost nepodporuje (je otevřena issue):

"""MCP klient, který přečte zvolený zdroj a získá z něj strukturovaná data."""
 
import json
from pprint import pprint
 
from mcp import ClientSession
from mcp.client.sse import sse_client
 
 
async def run():
    """Realizace klienta."""
    async with sse_client(url="http://localhost:8000/sse") as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
 
            # přečtení zdroje
            data = await session.read_resource("author://john-doe")
            print("Data returned:", data)
            print("Type:", type(data))
 
            text = data.contents[0].text
            print("Text:", text)
 
            deserialized = json.loads(text)
            print("Deserialized:")
            pprint(deserialized)
 
 
# přímé spuštění klienta
if __name__ == "__main__":
    import asyncio
 
    asyncio.run(run())

Po spuštění MCP klienta se nejdříve zobrazí celý objekt, který byl vrácen metodou session.read_resource. Tento objekt obsahuje sekvenci objektů typu TextResourceContents:

Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('author://john-doe'), mimeType='application/json', text='[{"name": "john", "surname": "doe", "title": "Foo", "year": 1900}, {"name": "john", "surname": "doe", "title": "Bar", "year": 2005}, {"name": "john", "surname": "doe", "title": "Baz", "year": 2025}]')]

Typ objektu:

Type: <class 'mcp.types.ReadResourceResult'>

Vlastní text přečtený z objektu je skutečně text:

Text: [{"name": "john", "surname": "doe", "title": "Foo", "year": 1900}, {"name": "john", "surname": "doe", "title": "Bar", "year": 2005}, {"name": "john", "surname": "doe", "title": "Baz", "year": 2025}]

Teprve po explicitním volání json.loads() (tedy po deserializaci) získáme z textu strukturovaná data se seznamem knih:

Deserialized:
[{'name': 'john', 'surname': 'doe', 'title': 'Foo', 'year': 1900},
 {'name': 'john', 'surname': 'doe', 'title': 'Bar', 'year': 2005},
 {'name': 'john', 'surname': 'doe', 'title': 'Baz', 'year': 2025}]
Poznámka: tuto deserializaci je nutné explicitně provádět vždy, pokud MCP server vrací strukturovaná data v JSONu.

6. Odlišný oddělovač selektorů

Samozřejmě můžeme server překonfigurovat tak, aby byly jednotlivé selektory zdroje odděleny lomítky a nikoli pomlčkami. Postačuje k tomu velmi malá změna. Dekorátor zdroje:

@mcp.resource("author://{name}-{surname}")

pozměníme na:

@mcp.resource("author://{name}/{surname}", mime_type="application/json")

Upravený zdrojový kód serveru vypadá následovně:

"""Jednoduchý MCP server s jediným definovaným dynamickým zdrojem."""
 
from mcp.server.fastmcp import FastMCP
 
# konstrukce serveru
mcp = FastMCP("Test")
 
 
@mcp.resource("author://{name}/{surname}", mime_type="application/json")
def author(name: str, surname: str) -> list:
    """Knihy od vybraného autora."""
    return [
        {"name": name, "surname": surname, "title": "Foo", "year": 1900},
        {"name": name, "surname": surname, "title": "Bar", "year": 2005},
        {"name": name, "surname": surname, "title": "Baz", "year": 2025},
    ]
 
 
# přímé spuštění serveru v režimu SSE (Server-Sent Events)
if __name__ == "__main__":
    mcp.run(transport="sse")

Samozřejmě je nutné změnit i klienta. Konkrétně se jedná o nahrazení řádku:

data = await session.read_resource("author://john-doe")

za řádek:

data = await session.read_resource("author://john/doe")

Opět si pro úplnost ukažme, jak klient vypadá po této nepatrné úpravě:

"""MCP klient, který přečte zvolený zdroj a získá z něj strukturovaná data."""
 
import json
from pprint import pprint
 
from mcp import ClientSession
from mcp.client.sse import sse_client
 
 
async def run():
    """Realizace klienta."""
    async with sse_client(url="http://localhost:8000/sse") as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
 
            # přečtení zdroje
            data = await session.read_resource("author://john/doe")
            print("Data returned:", data)
            print("Type:", type(data))
 
            text = data.contents[0].text
            print("Text:", text)
 
            deserialized = json.loads(text)
            print("Deserialized:")
            pprint(deserialized)
 
 
# přímé spuštění klienta
if __name__ == "__main__":
    import asyncio
 
    asyncio.run(run())

7. Chování MCP serveru po příchodu většího množství požadavků

V dalším zkoumání možností MCP se pokusíme zjistit, jakým způsobem MCP server reaguje na situaci, kdy dostane větší množství požadavků. Takový server tedy budeme muset spustit s transportem SSE a nikoli STDIO, protože komunikace přes STDIO je určena pro komunikaci pouze dvou procesů. Abychom odsimulovali nějakou složitější činnost MCP serveru, vložíme jak do volání nástroje (tool), tak i pro přístup ke zdrojům (resource) volání time.sleep s přibližně půlsekundovým intervalem. Tím budeme simulovat déletrvající operace. Nová podoba implementace MCP serveru bude vypadat následovně:

"""MCP server se zdrojem,  dynamickým zdrojem a definicí nástroje (tool)."""
 
from time import sleep
 
from mcp.server.fastmcp import FastMCP
 
# konstrukce serveru
mcp = FastMCP("Test")
 
SLEEP_AMOUNT=0.5
 
 
@mcp.tool()
def factorial(n: int) -> int:
    """Výpočet faktoriálu ve smyčce."""
    print("Factorial computation started")
    f = 1
    for x in range(1, n + 1):
        f *= x
    sleep(SLEEP_AMOUNT)
    print("Factorial computation finished")
    return f
 
 
@mcp.resource("pozdrav://")
def pozdrav1() -> str:
    """Odpověď s pozdravem."""
    print("Resource preparation started")
    sleep(SLEEP_AMOUNT)
    print("Resource preparation finished")
    return "Hello, dear client!"
 
 
@mcp.resource("pozdrav://{name}")
def pozdrav2(name: str) -> str:
    """Odpověď s osobním pozdravem."""
    print("Resource preparation started")
    sleep(SLEEP_AMOUNT)
    print("Resource preparation finished")
    return f"Hello, {name}!"
 
 
# přímé spuštění serveru v režimu SSE (Server-Sent Events)
if __name__ == "__main__":
    mcp.run(transport="sse")

8. Přístup ke „zpomalenému“ MCP serveru z MCP klienta

Abychom si ověřili, jak rychle nebo pomalu přístup k MCP serveru probíhá, přidáme do MCP klienta výpočet celkového času nutného pro přečtení obou zdrojů i výsledku vypočteného volaným nástrojem. Je to snadné:

"""MCP klient, který přečte zvolený zdroj a získá z něj data."""
 
from time import time
 
from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client
 
async def run():
    """Realizace klienta."""
    async with sse_client(url="http://localhost:8000/sse") as (read, write):
        async with ClientSession(read, write) as session:
            t1 = time()
            print("Client initialization")
            print()
 
            await session.initialize()
 
            # přečtení zdroje bez selektoru
            data = await session.read_resource("pozdrav://")
            print("Data returned:", data)
            print("Type:", type(data))
            text = data.contents[0].text
            print("Text:", text)
            print()
 
            # přečtení zdroje se selektorem
            data = await session.read_resource("pozdrav://Pavel")
            print("Data returned:", data)
            print("Type:", type(data))
 
            text = data.contents[0].text
            print("Text:", text)
            print()
 
            # zavolání nástroje
            data = await session.call_tool("factorial", arguments={"n": 10})
            factorial = data.content[0].text
            print("10!=", factorial)
            print()
 
            t2 = time()
            difftime = t2 - t1
            print(f"Client has finished in {difftime} seconds")
            print()
 
 
# přímé spuštění klienta
if __name__ == "__main__":
    import asyncio
 
    asyncio.run(run())

Takto může vypadat běh klienta:

Client initialization
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
10!= 3628800
 
Client has finished in 1.5324900150299072 seconds

Povšimněte si, že jeden MCP klient skončil skutečně za cca 1,5 sekundy (tedy co jedno volání, to přibližně 0,5 sekundy).

9. Souběžný přístup ke „zpomalenému“ MCP serveru

Nyní se pokusíme o souběžné spuštění deseti MCP klientů. Jsou možné dva výsledky:

  1. Každý MCP klient bude se obsloužen souběžně a tedy každý skončí přibližně po 1,5 sekundě
  2. Obsluha bude prováděna sekvenčně a časy budou mnohem delší – poslední klient by měl skončit po 10×1,5=15 sekundách
Poznámka: jedná se o minimální časy.

Princip spuštění:

for i in `seq 10`
do
    echo "Starting client #$i"
    python mcp_client_8.py > "$i.txt" &
done

Výsledek pro nejrychlejšího MCP klienta:

Client initialization
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
10!= 3628800
 
Client has finished in 3.0988974571228027 seconds

Výsledek pro nejpomalejší MCP klient:

Client initialization
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
10!= 3628800
 
Client has finished in 14.61089015007019 seconds

Z tohoto času (cca 1,5×10 sekund) vyplývá, že většina obsluhy klientů MCP serverem je prováděna sekvenčně a nikoli souběžně.

10. Asynchronní souběžný přístup ke „zpomalenému“ MCP serveru

Ovšem souběžný přístup je možné v případě potřeby zajistit i přímo v realizaci MCP klienta. Namísto jediného spuštění asynchronní funkce run jich necháme spustit několik. Pro tento účel si vytvoříme pomocnou asynchronní funkci main, v níž několikrát spustíme run a budeme čekat na všechny výsledky:

async def main():
    # spuštění několika klientů
    clients = [run(i) for i in range(10)]
    await asyncio.gather(*clients)

Upravený zdrojový kód klienta (včetně měření času) bude vypadat následovně:

"""MCP klient, který přečte zvolený zdroj a získá z něj data."""
 
from time import time
 
from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client
 
async def run(i):
    """Realizace klienta."""
    async with sse_client(url="http://localhost:8000/sse") as (read, write):
        async with ClientSession(read, write) as session:
            t1 = time()
            print(f"Client #{i} initialization")
            print()
 
            await session.initialize()
 
            # přečtení zdroje bez selektoru
            data = await session.read_resource("pozdrav://")
            print("Data returned:", data)
            print("Type:", type(data))
            text = data.contents[0].text
            print("Text:", text)
            print()
 
            # přečtení zdroje se selektorem
            data = await session.read_resource("pozdrav://Pavel")
            print("Data returned:", data)
            print("Type:", type(data))
 
            text = data.contents[0].text
            print("Text:", text)
            print()
 
            # zavolání nástroje
            data = await session.call_tool("factorial", arguments={"n": 10})
            factorial = data.content[0].text
            print("10!=", factorial)
            print()
 
            t2 = time()
            difftime = t2 - t1
            print(f"Client has finished in {difftime} seconds")
            print()
 
 
async def main():
    # spuštění několika klientů
    clients = [run(i) for i in range(10)]
    await asyncio.gather(*clients)
 
 
if __name__ == "__main__":
    import asyncio
 
    asyncio.run(main())

Opět se podívejme na výsledky, zejména na celkové časy:

Client #0 initialization
 
Client #1 initialization
 
Client #2 initialization
 
Client #7 initialization
 
Client #8 initialization
 
Client #9 initialization
 
Client #6 initialization
 
Client #4 initialization
 
Client #3 initialization
 
Client #5 initialization
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://'), mimeType='text/plain', text='Hello, dear client!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, dear client!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
Data returned: meta=None contents=[TextResourceContents(uri=AnyUrl('pozdrav://Pavel'), mimeType='text/plain', text='Hello, Pavel!')]
Type: <class 'mcp.types.ReadResourceResult'>
Text: Hello, Pavel!
 
10!= 3628800
 
Client has finished in 10.609342098236084 seconds
 
10!= 3628800
 
Client has finished in 15.126225709915161 seconds
 
10!= 3628800
 
Client has finished in 15.123616933822632 seconds
 
10!= 3628800
 
Client has finished in 15.123651266098022 seconds
 
10!= 3628800
 
Client has finished in 15.126556634902954 seconds
 
10!= 3628800
 
Client has finished in 15.12637448310852 seconds
 
10!= 3628800
 
Client has finished in 15.12638545036316 seconds
 
10!= 3628800
 
Client has finished in 15.126460552215576 seconds
 
10!= 3628800
 
Client has finished in 15.133534669876099 seconds
 
10!= 3628800
 
Client has finished in 15.127150774002075 seconds

I zde je patrné, že MCP server nedokáže obsloužit více požadavků současně.

11. Podpora rastrových obrázků v balíčku mcp

Knihovna mcp do jisté míry podporuje i posílání rastrových obrázků z MCP serveru klientovi. To je obecně užitečná vlastnost, protože mnoho generativních AI vytváří či modifikuje nějaké formy rastrových dat. K poslání rastrového obrázku MCP serverem se využívá knihovna Pillow resp. PIL (druhé jméno se používá z důvodů dodržení kompatibility). Má to však dvě omezení:

  1. Obrázky prozatím nelze použít ve funkci zdrojů (resource), což by však mělo být doimplementováno
  2. Obrázky je nutné na straně klienta dekódovat, což si pochopitelně ukážeme

12. MCP server posílající rastrový obrázek

Podívejme se nyní, jak bude vypadat realizace nástroje (tool), který jako výsledek své činnosti vytvoří a vrátí rastrový obrázek. Hlavička funkce s realizací takového nástroje vypadá následovně:

@mcp.tool()
def house() -> Image:

přičemž typ Image je importován z balíčku mcp.server.fastmcp.

Ve funkci zkonstruujeme nový binární obrázek o rozlišení 512×512 pixelů:

image = PILImage.new("1", (512, 512))

A vykreslíme do rastrového obrázku několik úseček – klasický domeček „jedním tahem“:

draw = ImageDraw.Draw(image)
...
...
...
draw.line(endpoints, fill=255)

Nyní nám již zbývá data rastrového obrázku vrátit klientovi. To je snadné:

    return Image(data=image.tobytes(), format="png")

Úplná implementace MCP serveru s nástrojem vracejícím obrázek vypadá takto:

"""MCP server se nástrojem s obrázkem."""
 
from PIL import Image as PILImage
from PIL import ImageDraw
 
from mcp.server.fastmcp import FastMCP, Image
 
# konstrukce serveru
mcp = FastMCP("Test")
 
 
#@mcp.resource("house://", mime_type="image/png")
@mcp.tool()
def house() -> Image:
    """Vrací rastrový obrázek."""
    # vytvoření prázdného obrázku
    image = PILImage.new("1", (512, 512))
 
    # objekt umožňující kreslení do obrázku
    draw = ImageDraw.Draw(image)
 
    endpoints = [
        100,
        500,
        400,
        200,
        100,
        200,
        250,
        50,
        400,
        200,
        400,
        500,
        100,
        200,
        100,
        500,
        400,
        500,
    ]
 
    draw.line(endpoints, fill=255)
 
    return Image(data=image.tobytes(), format="png")
 
 
# přímé spuštění serveru v režimu SSE (Server-Sent Events)
if __name__ == "__main__":
    mcp.run(transport="sse")

13. Dekódování obrázku na straně MCP klienta

Realizace MCP klienta, který přes MCP server zavolá nástroj a získá příslušný obrázek, vyžaduje explicitní programovou konverzi, kterou si ukážeme. Nejdříve pochopitelně zavoláme nástroj a přečteme data, která nástroj vrátil:

data = await session.call_tool("house")

Data jsou zakódována do BASE64 a poslána jako běžný text (to je ovšem zvláštní, protože existuje i možnost vrácení binárních dat). Proto provedeme získání první textové odpovědi, dekódování tohoto textu z BASE64 do binárních dat a rekonstrukce původního obrázku:

image = PILImage.frombytes('1', (512,512), base64.b64decode(data.content[0].data))

Nyní je již možné nechat si obrázek vykreslit na obrazovce, uložit do souboru atd.:

image.save("house.png")

Úplný zdrojový kód MCP klienta tedy může vypadat takto:

"""MCP klient, který přečte obrázek."""
 
 
import base64
 
from PIL import Image as PILImage
 
from mcp import ClientSession
from mcp.client.sse import sse_client
 
 
async def run():
    """Realizace klienta."""
    async with sse_client(url="http://localhost:8000/sse") as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
 
            data = await session.call_tool("house")
            image = PILImage.frombytes('1', (512,512), base64.b64decode(data.content[0].data))
            print(image)
            image.save("house.png")
 
 
# přímé spuštění klienta
if __name__ == "__main__":
    import asyncio

    asyncio.run(run())

Po inicializaci klienta by se měla vypsat „hlavička“ objektu reprezentujícího přečtený rastrový obrázek:

<PIL.Image.Image image mode=1 size=512x512 at 0x7F9589688E30>

Rastrový obrázek, který z MCP serveru získáme a uložíme do souboru ve formátu PNG, by měl vypadat takto:

domeček jedním tahem

Obrázek 1: Rastrový obrázek vráceny MCP serverem a uložený do souboru ve formátu PNG.

AI transformace

13. Další možnosti nabízené protokolem MCP

14. Repositář s demonstračními příklady

Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro Python a balíček mcp byly uloženy do repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V tabulce zobrazené níže jsou odkazy na jednotlivé příklady:

# Demonstrační příklad Stručný popis příkladu Cesta
1 mcp_server1.py jednoduchý MCP server s jediným definovaným zdrojem https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server1.py
2 mcp_server2.py jednoduchý MCP server s jediným definovaným dynamickým zdrojem https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server2.py
3 mcp_server3.py MCP server s dynamickým zdrojem a definicí nástroje (tool) https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server3.py
4 mcp_server4.py MCP server s jediným definovaným dynamickým zdrojem, zápis informací o spuštění https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server4.py
5 mcp_server5.py MCP server, který se přímo spustí v režimu STDIO https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server5.py
6 mcp_server6.py MCP server, který se přímo spustí v režimu SSE https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server6.py
7 mcp_server7.py MCP server se zdrojem vracejícím strukturovaná data https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server7.py
8 mcp_server8.py MCP server se zdrojem vracejícím strukturovaná data https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server8.py
9 mcp_server9.py MCP server, který dokáže poslat rastrový obrázek https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server9.py
10 mcp_server_A.py MCP server, který na požadavky odpovídá se zpožděním https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_server_A.py
     
11 mcp_client1.py MCP klient, který spustí server, se kterým se komunikuje přes STDIO https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client1.py
12 mcp_client2.py MCP klient, který spustí server, se kterým se komunikuje přes SSE https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client2.py
13 mcp_client3.py MCP klient, který přečte zvolený zdroj https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client3.py
14 mcp_client4.py MCP klient, který přečte zvolený zdroj a získá z něj data https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client4.py
15 mcp_client5.py MCP klient, který zavolá vybraný nástroj https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client5.py
16 mcp_client6.py MCP klient, který přečte a zpracuje strukturovaná data https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client6.py
17 mcp_client7.py MCP klient, který dokáže načíst rastrový obrázek https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client7.py
18 mcp_client8.py MCP klient s měřením času odpovědí MCP serveru https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client8.py
19 mcp_client9.py MCP klient s měřením času odpovědí MCP serveru a více asynchronními voláními https://github.com/tisnik/most-popular-python-libs/blob/master/mcp/mcp_client9.py

15. Příloha: články o knihovně PIL/Pillow

Dnes jsme se na okraj zmínili o knihovně PIL resp. Pillow, která v Pythonu slouží pro manipulaci s rastrovými obrázky, a to včetně načítání a ukládání těchto obrázků do souborů ve vybraných formátech (PNG, GIF, JPEG, PBM atd.), ale i pro kreslení jednoduchých 2D tvarů. Možnostmi poskytovanými touto knihovnou jsem se zabýval v následující dvojici článků:

  1. Užitečné knihovny a moduly pro Python: kreslení a pokročilé manipulace s obrázky v knihovně Pillow
    https://mojefedora.cz/uzitecne-knihovny-a-moduly-pro-python-kresleni-a-pokrocile-manipulace-s-obrazky-v-knihovne-pillow/
  2. Užitečné knihovny a moduly pro Python: knihovna Pillow určená pro manipulaci s rastrovými obrázky
    https://mojefedora.cz/uzitecne-knihovny-a-moduly-pro-python-knihovna-pillow-urcena-pro-manipulaci-s-rastrovymi-obrazky/

16. Odkazy na Internetu

  1. MCP Python SDK
    https://github.com/modelcon­textprotocol/python-sdk?tab=readme-ov-file#running-your-server
  2. MCP protocol: Resources
    https://modelcontextproto­col.info/docs/concepts/re­sources/
  3. Example Servers
    https://modelcontextproto­col.io/examples
  4. Core architecture
    https://modelcontextproto­col.io/docs/concepts/archi­tecture
  5. Unleashing the Power of Model Context Protocol (MCP): A Game-Changer in AI Integration
    https://techcommunity.micro­soft.com/blog/educatordeve­loperblog/unleashing-the-power-of-model-context-protocol-mcp-a-game-changer-in-ai-integrat/4397564
  6. MPC inspector
    https://github.com/modelcon­textprotocol/inspector
  7. Model Context Protocol servers
    https://github.com/modelcon­textprotocol/servers
  8. python-sdk na GitHubu
    https://github.com/modelcon­textprotocol/python-sdk
  9. typescript-sdk na GitHubu
    https://github.com/modelcon­textprotocol/typescript-sdk
  10. mcp-golang
    https://github.com/metoro-io/mcp-golang
  11. MCP server: A step-by-step guide to building from scratch
    https://composio.dev/blog/mcp-server-step-by-step-guide-to-building-from-scrtch/
  12. How to Build an MCP Server Fast: A Step-by-Step Tutorial
    https://medium.com/@eugenesh4work/how-to-build-an-mcp-server-fast-a-step-by-step-tutorial-e09faa5f7e3b
  13. Step-by-Step Guide: Building an MCP Server using Python-SDK, AlphaVantage & Claude AI
    https://medium.com/@syed_hasan/step-by-step-guide-building-an-mcp-server-using-python-sdk-alphavantage-claude-ai-7a2bfb0c3096
  14. RFC 6570: URI Template
    https://datatracker.ietf.or­g/doc/html/rfc6570
  15. Return resources as structured JSON instead of text?
    https://github.com/modelcon­textprotocol/python-sdk/issues/279
  16. Python standard library: pprint
    https://docs.python.org/3/li­brary/pprint.html
  17. Python standard library: json — JSON encoder and decoder¶
    https://docs.python.org/3/li­brary/json.html
  18. Calling MCP Servers the Hard Way
    https://deadprogrammersoci­ety.com/2025/03/calling-mcp-servers-the-hard-way.html
  19. mcptools
    https://github.com/f/mcptools
  20. Server-sent events
    https://en.wikipedia.org/wiki/Server-sent_events
  21. Model context protocol (MCP)
    https://openai.github.io/openai-agents-python/mcp/
  22. A Clear Intro to MCP (Model Context Protocol) with Code Examples
    https://towardsdatascience.com/clear-intro-to-mcp/
  23. A Developer's Guide to the MCP
    https://www.getzep.com/ai-agents/developer-guide-to-mcp
  24. MCP: Flash in the Pan or Future Standard?
    https://blog.langchain.dev/mcp-fad-or-fixture/
  25. MCP yeah you know me: A complete guide and review of Model Context Protocol (MCP)
    https://ebi.ai/blog/model-context-protocol-guide/
  26. Pillow documentation
    https://pillow.readthedoc­s.io/en/stable/handbook/tu­torial.html
  27. Pillow: Python Imaging Library (Fork)
    https://pypi.org/project/pillow/
  28. How to use Pillow, a fork of PIL
    https://www.pythonforbegin­ners.com/gui/how-to-use-pillow
  29. PNG is Not GIF
    https://www.root.cz/clanky/png-is-not-gif/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.