Poveži Agent-u MCP, podržava stdio i Streamable HTTP.
Prošli put smo dodali PaiCLI-u mrežne mogućnosti, pretraga i čitanje stranica postoje. Ali ovi alati smo svi sami implementirali.
Ako hoćemo da Agent operiše lokalni fajl sistem?
Ili čita GitHub repozitorijume?
Za svaku potrebu da sam napišem od početka?
Ne treba. MCP može da se koristi za ovaj posao.

MCP puni naziv Model Context Protocol, prevedeno znači protokol konteksta modela. Jednom rečenicom sažeto: to je standardni protokol koji Agent-u omogućava da se poveže sa alatima treće strane.
Sa MCP-om, alati koje su drugi napisali (npr. fajl operacije, GitHub čitanje, baza podataka upit), naš Agent ne mora da napiše jednu liniju koda da bi ih koristio.
Naravno, konfiguracija ipak treba.😄
Ovaj put, dođimo da povežemo PaiCLI sa MCP-om.
01, prvo pogledaj rezultat
Stari pravilnik, prvo pogledaj šta PaiCLI može da radi nakon povezivanja MCP-a, onda pričaj kako da se realizuje.
Pre konfigurisanja MCP-a, pri pokretanju PaiCLI-a videćeš ovakav hint:

🔌 MCP server: nekonfigurisan. Znači MCP je spreman, samo treba da kažemo koje server-e treba povezati.
Ovaj put ćemo povezati dva MCP server-a.
Prvi je Claude zvanično obebeđen filesystem, alat za lokalne fajl operacije baziran na stdio, podržava čitanje i pisanje fajlova, pretragu fajlova, listanje direktorijuma itd.
stdio šta znači, kasnije ćemo pričati.
Drugi je Zhipu obebeđen zread, alat za daljinski čitanje baziran na Streamable HTTP, može da čita sadržaj fajlova i strukturu direktorijuma GitHub repozitorijuma.
Konfiguracioni fajl se stavlja u ~/.paicli/mcp.json, sadržina izgleda ovako:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "${PROJECT_DIR}"]
},
"zread": {
"type": "http",
"url": "https://open.bigmodel.cn/api/mcp/zread/mcp",
"headers": {
"Authorization": "Bearer your_api_key"
}
}
}
}
Dobro, naše PaiCLI Agent je istovremeno povezao dva tipa MCP-a: jedan je stdio transport (filesystem), drugi je Streamable HTTP transport (zread).
Ponovo pokreni PaiCLI, unesi prompt:
Pročitaj README iz korenog direktorijuma itwanger/paicoding repozitorijuma, kaži mi šta radi

Agent je automatski pozvao zread.read ovaj MCP alat, sa GitHub-a dohvatio sadržaj README-a, onda dao sumiranje.

Još probaj zread.get_repo_structure, unesi prompt: Hoću da vidim kompletnu stablastu strukturu src direktorijuma, uključujući sve poddirektorijume i fajlove.

Kompletna struktura direktorijuma repozitorijuma je jasna. Možeš probati i druge promte:
- Pretraži zvaničnu dokumentaciju itwanger/paicli ovog repozitorijuma, vidi kako je MCP integrisan?
- Vidi kompletnu strukturu direktorijuma itwanger/paicoding ovog repozitorijuma
Sledeće testiraj Claude zvanični filesystem MCP.
Najbolje počisti kontekst /clear.
Unesi prompt: U README.md trećoj liniji tu reč "usklađen sa Claude Code" promeni u "usklađen sa Claude Code autor je Marko Marković".

Ovde vidiš da je Agent pozvao MCP alat filesystem.edit_file, direktno izmenio lokalni fajl.
Vrati se u repozitorijum da verifikuješ, izmena zaista važi.

Još jedna primer, pozovi filesystem.search_files, u celom repozitorijumu pretraži Java fajlove čije ime sadrži Manager.

Oba tipa MCP-a su prošla.
Agent ne treba da zna detalje implementacije alata, samo ako MCP server obezbeđi opis alata, Agent može na osnovu namera korisnika automatski birati i pozivati.
Ovo je najveća vrednost MCP-a: razvoj alata i razvoj Agent-a mogu potpuno da se dekopleju. Autor filesystem-a ne treba da zna kako je PaiCLI implementiran, PaiCLI ne treba da zna kako filesystem interno čita i piše fajlove. Obje strane samo treba da prate MCP protokol, mogu bezbedno da se povežu.
02, šta je MCP uopšte
Efekat smo gledali, sada dođimo da tvrdimo, detalje MCP tehnički detaljno objasnimo.
JSON-RPC 2.0 protokol
Osnovni komunikacioni protokol MCP-a je JSON-RPC 2.0.
Nije HTTP REST, nije gRPC, samo najobičniji JSON-RPC.
JSON-RPC je vrlo jednostavan, jedan zahtev izgleda ovako:
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}Jedan odgovor izgleda ovako:
{"jsonrpc": "2.0", "id": 1, "result": {"tools": [...]}}id se koristi za uparivanje zahteva i odgovora. Klijent šalje zahtev id=1, server vraća odgovor id=1, čak i ako više zahteva konkurentno šalje, neće se pomešati.
Postoji još jedan tip koji se zove notifikacija (notification), nema id polje, server ne treba da odgovara:
{"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}Zašto birati JSON-RPC a ne REST?
Zato što komunikacija MCP-a nije resurs-orjentisana (ne postoji "URL određenog alata"), već poziv procedure-orjentisana——"hoću da pozovem određenu metodu, prosledim ove parametre".
JSON-RPC prirodno odgovara ovoj sceni, protokol glava i rep zajedno ima samo nekoliko polja, parsiranje je mnogo jednostavnije od HTTP REST.

PaiCLI je ručno napisao JsonRpcClient, oko 120 linija koda. Koristi AtomicLong da generiše rastući ID, koristi ConcurrentHashMap<Long, CompletableFuture<JsonNode>> da radi uparivanje zahteva-odgovora. Prilikom slanja zahteva stavlja future u mapu, prilikom primanja odgovora po ID-u pronalazi future i complete, koristi future.get(timeout) čeka rezultat.
Kontrola time-out-a podrazumevana 60 sekundi, koristi ScheduledExecutorService (daemon nit) da zakazuje periodične zadatke, kad dodaje vreme, pending future će se završiti sa TimeoutException izuzetkom. Time-out koji je postavio pozivatelj će imati 1 sekundu više od internog, izbjegava se trka između scheduler-a i pozivaoca.
Dva transportna načina
MCP protokol definiše dva transportna načina: stdio i Streamable HTTP.

stdio je "pokrenuti podproces, komunicirati kroz stdin/stdout".
Agent je roditelj proces, MCP server je dete proces, dvoje komuniciraju kroz cev. Svaki put šalje jednu JSON-RPC poruku, zato se zove NDJSON (Newline-Delimited JSON).
Prednost ovog načina je jednostavnost i direktnost, ne treba mreža, kašnjenje je minimalno.
Claude zvanični filesystem, Git, SQLite ovi MCP svi su stdio. Mana je da server mora da radi na tvojoj lokalnoj mašini, ne može daljinski pozivati.
stdio još ima jednu ne tako očiglednu prednost: bezbednost.
Zato što server je sam pokrenuti podproces, njegove dozvole su iste kao PaiCLI proces, ne postoji rizik mrežne ekspozicije. Ne treba brinuti da neko preko mreže poziva tvoj MCP server.

Streamable HTTP je marta 2025. novo definisani transportni standard (zamenjuje prethodni ukinuti čisti SSE plan).
Jednostavno rečeno "šalje POST zahtev na URL, server sa SSE (Server-Sent Events) strimira odgovor". Ovaj način podržava daljinski server, Zhipu-ov zread je ovakav.
Prednost Streamable HTTP-a je da server može da se deploy-uje na oblak, više klijenata deli istu instancu server-a. Mana je što dodaje mrežno kašnjenje, i treba da radi autentikaciju (ako svako može da poziva tvoj server, to je problem).
U zaglavlju zahteva će se nositi verzija protokola: MCP-Protocol-Version: 2025-03-26. Odgovor server-a može biti običan JSON (jednokratno vraća), ili text/event-stream (SSE strimano vraća). Klijent na osnovu Content-Type zaglavlja određuje kako da parsira.
Streamable HTTP još ima mehanizam Session ID. Server u zaglavlju odgovora vraća Mcp-Session-Id, klijent naredni zahtev nosi ovaj ID, server može da prati stanje sesije. Prilikom zatvaranja šalje DELETE zahtev da obavesti server da čisti.
Inicijalizacioni handshake
Bez obzira koji transportni način, prva stvar između MCP klijenta i server-a je handshake.
Klijent šalje initialize zahtev, kaže server-u "ko sam ja, šta mogu, koju verziju protokola koristim":
{
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": { "tools": {} },
"clientInfo": { "name": "paicli", "version": "10.0.0" }
}
}Server vraća svoje informacije i deklaraciju mogućnosti. Nakon uspešnog handshake-a, klijent šalje još jednu notifications/initialized notifikaciju, znači "spreman sam, može početi rad".
Dva-step handshake dizajn je radi kompatibilnosti.

Kada se verzije protokola ne poklapaju, u initialize fazi može se odbiti, ne treba čekati do poziva alata da se javi greška. Dodatno capabilities deklariše koje mogućnosti klijent podržava (trenutno PaiCLI samo deklariše tools), server na osnovu toga može odlučiti koje funkcionalnosti izložiti. Npr. ako u budućnosti server podržava resources i prompts, ali klijent nije deklarisao podršku, server može da bira ne izlaže te mogućnosti, izbjegava probleme kompatibilnosti.
PaiCLI-jev initialize time-out podešen na 30 sekundi, upola manji od običnog tools/call (60 sekundi). Zato što handshake treba biti lagana operacija, ako 30 sekundi još uvek nema odgovor, verovatno je mrežni problem ili server pao, nema smisla čekati dalje.
tools/list i tools/call
Nakon završetka handshake-a, prva stvar klijenta je pitati server "koji alate imaš":
{"method": "tools/list", "params": {}}Server vraća listu alata, svaki alat sadrži ime, opis, JSON Schema parametara. Npr. filesystem-ov read_file alat:
{
"name": "read_file",
"description": "Read the complete contents of a file",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "Path to file" }
},
"required": ["path"]
}
}Nakon što dobije listu alata, PaiCLI će svaki alat registrovati u ToolRegistry, tako da LLM prilikom Function Calling može da vidi ove alate.
Poziv alata je još jednostavniji:
{"method": "tools/call", "params": {"name": "read_file", "arguments": {"path": "/README.md"}}}Server nakon izvršenja vraća content niz, unutra je izlaz alata. PaiCLI spaja tekst sadržine i šalje natrag LLM-u, netekst sadržine (npr. slike) daje placeholder hint.
03, PaiCLI-jeva MCP implementacija
Princip smo objasnili, sada pogledajmo kako PaiCLI ovu stvar realizuje.
Cela MCP podsistem smešten je u com.paicli.mcp paket, arhitektura podeljena u četiri sloja:

Transportni sloj
McpTransport je interfejs, samo dva core metoda: send(JsonNode) šalje poruku, onReceive(Consumer<JsonNode>) prima poruku. Bez obzira da li je ispod cev ili HTTP, gornji kod ne treba da brine.
StdioTransport koristi ProcessBuilder da pokrene podproces.
Nakon pokretanja otvara dve daemon niti: jedna neprestano čita stdout (svaku liniju parsira u JSON i emitira listener-a), druga čita stderr (upisuje u 200-linijski kružni bafer, sprečava log da eksplodira memoriju).

Prilikom zatvaranja podprocesa koristi tri-step degradacijsku strategiju: prvo zatvori stdin (daj server-u šansu da elegantly izađe, čeka 1 sekund) → ako nije izašao šalje SIGTERM (još čeka 2 sekunde) → još uvek nije izašao SIGKILL prisilno ubija.
Ovaj dizajn se referiše na Docker-ovu strategiju zaustavljanja kontejnera, garantuje da podproces neće postati zombi.
process.getOutputStream().close(); // 1. zatvori stdin, hint "treba da izađeš"
if (!process.waitFor(1, TimeUnit.SECONDS)) {
process.destroy(); // 2. SIGTERM
if (!process.waitFor(2, TimeUnit.SECONDS)) {
process.destroyForcibly(); // 3. SIGKILL
}
}
StreamableHttpTransport koristi OkHttp da šalje POST zahtev. Ako je Content-Type odgovora text/event-stream, parsira se po SSE formatu (akumulira data: linije, nađe praznu liniju pa spaja u kompletnu poruku); inače parsira kao običan JSON.
Prilikom zatvaranja šalje DELETE zahtev, nosi Session ID, obaveštava server da čisti sesiju.
Ovo je best-effort, ako ne uspe neće blokirati PaiCLI izlaz.
Paralelno pokretanje više Server-a
McpServerManager upravlja životnim ciklusom svih MCP server-a. Prilikom pokretanja nije jedan po jedan serijiski pokreće, već koristi thread pool paralelno pokreće.
ExecutorService pool = Executors.newFixedThreadPool(
Math.min(targets.size(), 8),
r -> { Thread t = new Thread(r, "paicli-mcp-startup-" + id); t.setDaemon(true); return t; }
);Zašto paralelno?
Zato što stdio tip MCP server-a često se pokrece kroz npx, prvo pokretanje npx-a će skinuti zavisnosti, može trebati nekoliko sekundi čak i desetak sekundi.
Ako konfigurišeš 5 server-a serijaski pokretanje, samo čekanje će trajati jedan minut. Paralelno pokretanje, svi istovremeno skidaju i inicijalizuju, ukupno vreme zavisi samo od najsporijeg.

Gornja granica thread pool-a je 8, daemon nit, neće blokirati JVM izlaz. Ovde namerno nije korišćen ForkJoinPool.commonPool(), zato što je common pool globalno deli, ako MCP hladno pokretanje zauzme, utiče na druge funkcionalnosti.
Pokretanje svakog server-a je međusobno izolovano. A server pokretanje ne uspe, stanje postaje ERROR, ali ne utiče na B server normalno pokretanje. Alati će takođe po server granuli registrovati i odregistrovati.
Registracija alata i imenski prostor
Alat koje MCP server obezbeđuje prilikom registracije u ToolRegistry, dodaje prefiks imenskog prostora: mcp__{server ime}__{alat ime}.
Npr. filesystem server-ov read_file alat, ime registracije u ToolRegistry je mcp__filesystem__read_file. zread server-ov read alat se zove mcp__zread__read.

Zašto dodati prefiks?
Dva razloga.
Prvi je izbjegavanje konflikta. PaiCLI sam ima read_file alat, filesystem MCP takođe ima read_file alat, ako ne doda prefiks ime se ponavlja.
Drugi je bezbednosni audit. Svi pozivi alata koji počinju sa mcp__ će se automatski beležiti u audit log, pogodno za naknadan tracking. I HITL ljudsko odobrenje mod će po default-u otvoriti odobrenje za sve MCP alate, zato što alati treće strane nisu pouzdani.
// ApprovalPolicy u logici odluke
public boolean requiresApproval(String toolName) {
return DANGEROUS_TOOLS.contains(toolName) || isMcpTool(toolName);
}
private boolean isMcpTool(String toolName) {
return toolName != null && toolName.startsWith("mcp__");
}Ovo znači da MCP alati i PaiCLI ugrađeni opasni alati (write_file, execute_command) uživaju isti nivo bezbednosne kontrole. Audit log će beležiti svaki poziv alat ime, parametre, rezultat, trajanje, osetljivi parametri (Bearer token, password itd.) će automatski biti maskirani.
Schema čišćenje
Schema parametara alata koje MCP server vraća je raznovrsno, neki Schema sadrže $ref, anyOf, oneOf ove napredne osobine, LLM ne nužno može ispravno da procesira.

PaiCLI je implementirao McpSchemaSanitizer, pre registracije alata radi jedan krug čišćenja Schema:
Obriši $schema, $id, $ref ove polja koje LLM ne razume. anyOf, oneOf razvij i napiši kao description tekst opis.
Osiguraj da svaki Schema ima type i properties polja. Opis duži od 1000 znakova sredi. Ne-object tip Schema-u celog wrap-a u object.
Ovaj cleaner je oko 100 linija koda, ali rešava mnogo stvarnih problema. Bez njega, mnogo MCP server vraća Schema će LLM generisati parametre pogrešnog formata, dovodi do tools/call neuspela.
Jedan stvarni primer:
Neki MCP server vraća Schema-u tip polje nedostaje, samo properties.
LLM vidi nema tip ne zna da generisati object ili string, rezultat slučajno izbacije string, server parsiranje ne uspe. McpSchemaSanitizer će nedostajuće automatski dopuniti {"type": "object"}, ova granična situacija neće više biti problem.
04, konfiguracija i korišćenje
Konfiguracioni fajl
MCP konfiguracioni fajl smešten je u dva mesta:
- Korisnički nivo:
~/.paicli/mcp.json(globalno važi) - Projektni nivo:
.paicli/mcp.json(samo tekući projekat važi, prioritet je viši)
Projektna konfiguracija može se commit-ovati u git, timski članovi clone-down mogu direktno koristiti. Ovo je takođe moj razlog dizajna dva-nivo konfiguracije: korisnički nivo stavlja lične preferencije (npr. tvoj API Key), projektni nivo stavlja timski deljeni server (npr. interni knowledge base MCP kompanije).

Redosled učitavanja je prvo čita korisnički, onda projektni. Ako isto ime server u oba fajla postoji, projektni će preklopiti korisnički.
Format konfiguracije je kompatibilan sa Claude Code-ovim claude_settings_config.json. Možeš iz Claude Code prepisati MCP konfiguraciju, promeni ime fajla možeš koristiti.
stdio tip konfiguracija
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "${PROJECT_DIR}"],
"env": { "NODE_OPTIONS": "--max-old-space-size=256" }
}
}
}command je komanda za izvršenje, args su parametri, env su promenljive okruženja.
${PROJECT_DIR} i ${HOME} su ugrađene promenljive, prilikom pokretanja automatski zamenjuju. Takođe možeš koristiti ${VARNAME} da referiraš sistemske promenljive okruženja.
Zamena promenljivih je svaki server nezavisno radio, određeni server zamena promenljive ne uspe samo utiče na sebe, ne će povući druge server-e.
Streamable HTTP tip konfiguracija
{
"mcpServers": {
"zread": {
"url": "https://open.bigmodel.cn/api/mcp/zread/mcp",
"headers": {
"Authorization": "Bearer ${GLM_API_KEY}"
}
}
}
}url je adresa MCP server-a, headers su custom zaglavlja zahteva. Ovde API Key napišeš kao ${GLM_API_KEY}, čita iz promenljive okruženja, ne treba u konfiguracioni fajl jasno pisati ključ.
CLI komanda
PaiCLI obezbeđuje grupu /mcp komandi da upravlja MCP server-om:
/mcp Pregled stanja svih server-a
/mcp restart <name> Restartuj određeni server
/mcp logs <name> Pregled stderr log-a određenog server-a
/mcp disable <name> Onemogući određeni server
/mcp enable <name> Ponovo omogući određeni server/mcp će izbaciti tablicu stanja, sadrži ime svakog server-a, stanje (● ready / ✗ error / ○ disabled), transportni način, broj alata, dužinu rada, PID procesa (samo stdio tip).

Npr. nakon uspešnog pokretanja filesystem videćeš ● ready | stdio | 11 tools | 3m | PID 12345, zread je ● ready | http | 3 tools | 3m. Ako neki server pokaže ✗ error i doda razlog greške.
/mcp logs filesystem može videti filesystem server-ov stderr izlaz, najviše zadržava nedavne 200 linija. npx hladno pokretanje download log, server interni debug informaciju svi ovde mogu videti, problema rešavati vrlo pogodno.
05, PaiCLI kako da napišeš na CV?
Nakon što naučiš ovaj broj, na CV možeš ovako napisati:
- Naziv projekta: PaiCLI - Agent CLI
- Projekat opis: Alatka naredbene linije Agent-a proizvodnog nivoa izgrađena od nule, podržava mrežnu pretragu, čitanje veba, MCP protokol, multi Agent saradnju itd. sposobnosti
- Tehnološki stack: Java 21, JSON-RPC 2.0, OkHttp, SSE, ProcessBuilder, MCP 2025-03-26 specifikacija
- Core odgovornosti:
- Ručno napisan JSON-RPC 2.0 klijent, baziran na CompletableFuture + ConcurrentHashMap realizovao asinhrono uparivanje zahteva-odgovora, podržava time-out scheduling i notifikaciju broadcast
- Dizajnirao McpTransport transportni sloj apstrakcije, realizovao stdio (podproces cev komunikacija) i Streamable HTTP (OkHttp + SSE striming parsiranje) dva transportna načina
- Realizovao kompletan životni ciklus MCP protokola: initialize handshake + capabilities pregovor + tools/list otkriće alata + tools/call poziv alata
- Dizajnirao McpServerManager multi Server paralelno pokretanje framework, baziran na fiksnom thread pool-u i CompletableFuture.allOf() realizovao ubrzanje pokretanja, jednog Server kvar ne utiče na globalno
- Realizovao MCP alat imenski prostor izolaciju (mcp__server__tool), sa HITL odobrenjem i AuditLog audit sistemom saradnju, alati treće strane po default-u uključeni u bezbednosnu kontrolu
ending
Adresa izvornog koda projekta: https://github.com/itwanger/paicli, 10. broj koda je sve commit-ovano. Dobrodošli star, fork, podnesi issue.

Jedan filesystem, Agent može operisati tvoje lokalne fajlove.
Jedan zread, Agent može razumeti ceo GitHub repozitorijum.
Jedan Chrome DevTools MCP, Agent može otvoriti pretraživač (kasnije ćemo pričati).
[Znacenje pravljenja tocka od nule, nije reinventiranje, nego znati zašto je tocka okrugla.]
Vidimo se sledeći put.
