অধ্যায় 2 · MCP সার্ভার, ক্লায়েন্ট ও আন্তঃকার্যক্ষমতা
MCP আর্কিটেকচার ও লোকাল সার্ভার
- পৃষ্ঠা 3 / 22
- 22 মিনিট পড়া
কায়রোর ওমর সাপোর্ট অ্যাসিস্ট্যান্ট চালান। সেলস টিম নিজেদের টুলে একই হেল্প-সেন্টার লুকআপ চায়, আর তৃতীয় একটা টিম সেটা এডিটরের ভেতরে চায়। টুল চুক্তি ও এক্সিকিউশন পাতার রেজিস্ট্রি তিনটা কোড বেসে কপি করলে প্রতিটা বাগ তিন জায়গায় ঠিক করতে হয়। Model Context Protocol (MCP) বরং টুলগুলোই ভাগ করে নেয়: একটা ছোট সার্ভার, অনেক ক্লায়েন্ট। তবে এটা একটা নতুন সীমানা, নতুন ধরনের ব্যর্থতা সহ, আর প্রটোকলটাও এখনো বদলাচ্ছে। এই পাতায় সবচেয়ে ছোট সৎ সংস্করণ বানানো হয়েছে: একটা রিড-অনলি সার্ভার, একটা ক্লায়েন্ট, আর মডেল ছাড়াই চলে এমন টেস্ট।
সবকিছু লোকাল: কৃত্রিম Kestrel হেল্প-সেন্টারের ফাইল, ইন-মেমরি সংযোগ, কোনো মডেল নেই। আসল মডেল কম অনুমানযোগ্য, আর ঠিক এ কারণেই এখানকার টেস্ট সরাসরি টুল কল করে।
যা শিখবেন
- চারটা ভূমিকা (মডেল, হোস্ট, ক্লায়েন্ট, সার্ভার), কখন প্রটোকল সীমানা লোকাল রেজিস্ট্রির চেয়ে ভালো, আর MCP-র টুল, রিসোর্স ও প্রম্পট কীসের জন্য।
- বর্তমান প্রটোকল ডকুমেন্ট কীভাবে পড়বেন, পরিবর্তনের জন্য কীভাবে তৈরি থাকবেন, আর ড্রিফট চেকসহ ভার্সন কীভাবে পিন করবেন।
- একটা রিড-অনলি লোকাল সার্ভার ও ক্লায়েন্ট কীভাবে বানাবেন ও টেস্ট করবেন, আর প্রটোকল যে চুক্তি-চেকগুলো করে না সেগুলো কীভাবে যোগ করবেন।
- MCP টুলকে নিজের টুল রেজিস্ট্রিতে কীভাবে ম্যাপ করবেন, যাতে অ্যাপ্লিকেশনের বাকি অংশ কোনো প্রটোকল অবজেক্ট না দেখে।
চারটা ভূমিকা, একটা সীমানা
AI অ্যাপ্লিকেশনে MCP-র ভূমিকা। চারটা পক্ষ সহজেই গুলিয়ে যায়। মডেল শুধু টেক্সট তৈরি করে, টুল কলের অনুরোধও সেই টেক্সটের অংশ। হোস্ট হলো আপনার অ্যাপ্লিকেশন: সে কনটেক্সট বানায়, ক্লায়েন্ট তৈরি করে, নীতি ও সম্মতি কার্যকর করে এবং কী চলবে তা ঠিক করে। MCP ক্লায়েন্ট হোস্টের ভেতরে থাকে এবং ঠিক একটা সার্ভারের সঙ্গে কথা বলে। MCP সার্ভার টুল, রিসোর্স ও প্রম্পট দেয়, লোকাল প্রসেস হিসেবে অথবা রিমোট সার্ভিস হিসেবে।
model --text: "call lookup_document"--> HOST (your app: context, policy, consent)
| one client per server
CLIENT --JSON-RPC over stdio / Streamable HTTP--> SERVER
tools, resources, promptsমডেল কখনো সার্ভারের সঙ্গে কথা বলে না। হোস্ট মডেলের অনুরোধকে ক্লায়েন্ট কলে রূপ দেয়, ঠিক যেভাবে টুল চুক্তি ও এক্সিকিউশন পাতার রেজিস্ট্রি অনুরোধকে কাজে রূপ দিয়েছিল। MCP শুধু টুল কোথায় থাকে তা বদলায়; অনুরোধ যাচাইয়ের দায়িত্ব কার, তা বদলায় না।
MCP নাকি সরাসরি ফাংশন কলিং?
MCP ও সরাসরি ফাংশন কলিংয়ের তুলনা। সরাসরি কলিং মানে নিজের প্রসেসের ভেতরের একটা রেজিস্ট্রি (ToolRegistry, সঙ্গে টুলের অনুমতি ও ব্যর্থতা সামলানো পাতার অ্যালাউলিস্ট, সীমা ও অডিট ইভেন্ট)। MCP হোস্ট ও টুলের মাঝখানে একটা প্রসেস বা নেটওয়ার্ক সীমানা বসায়।
| প্রশ্ন | লোকাল রেজিস্ট্রি | MCP-র মাধ্যমে |
|---|---|---|
| কারা টুলটা পুনর্ব্যবহার করতে পারে? | একটা কোড বেস | প্রটোকল বোঝে এমন যেকোনো হোস্ট |
| ভাষা ও রিলিজ চক্র | অ্যাপের মতোই | স্বাধীন: অন্য টিম, অন্য ভাষা |
| নতুন ব্যর্থতা | কম: নিজের এক্সসেপশন | সংযোগ বিচ্ছিন্ন, টাইমআউট, ভার্সন অমিল, ভুল গঠনের (malformed) বা শত্রুভাবাপন্ন রিপ্লাই |
| আর্গুমেন্ট ও ফলাফল কে যাচাই করে? | আপনার Pydantic মডেল | দুই পক্ষ; অন্য পক্ষ যাচাই করেছে ধরে নেওয়া যাবে না |
| বিশ্বাস | আপনার কোড | অন্যের কোড ও টেক্সট (বর্ণনা, ফলাফল) |
| চালানোর খরচ | বাড়তি কিছু নেই | ডিপ্লয়, মনিটর ও প্যাচ করার মতো একটা প্রসেস বা সার্ভিস |
সাধারণ নিয়ম: একটা অ্যাপ্লিকেশন যতক্ষণ টুলের মালিক, লোকাল রেজিস্ট্রি ব্যবহার করুন। প্রটোকল সীমানায় যান যখন একাধিক হোস্টকে টুলটা ভাগ করতে হয়, অন্য টিম মালিক, অথবা টুলটাকে নিজস্ব প্রসেসে আলাদা করে রাখতে চান। সিদ্ধান্তটা মালিকানা ও পুনর্ব্যবহারের, কোনটা নতুন তার নয়। নিচে একই পাঁচটা কল দুই পথে চালানো হয়েছে, যাতে সীমানা কী যোগ করে তা দেখা যায়; পরের পাতা রক্ষণাবেক্ষণের খরচে ফিরে আসবে।
টুল, রিসোর্স ও প্রম্পট
MCP টুল, রিসোর্স ও প্রম্পট ঘুরে দেখুন। সার্ভারের তিনটা ফিচার আলাদা, কারণ কে সেটা ব্যবহার করার সিদ্ধান্ত নেয় তা আলাদা:
| ফিচার | নিয়ন্ত্রণ করে | এটা কী | আমাদের সার্ভারে |
|---|---|---|---|
| টুল | মডেল (হোস্টের মাধ্যমে) | এমন ফাংশন যা কিছু করে ও ফলাফল ফেরত দেয় | lookup_document, search_help |
| রিসোর্স | অ্যাপ্লিকেশন | URI দিয়ে চিহ্নিত কনটেক্সট ডেটা | kestrel://index, kestrel://docs/{doc_id} |
| প্রম্পট | ব্যবহারকারী | পুনর্ব্যবহারযোগ্য টেমপ্লেট, স্ল্যাশ কমান্ডের মতো | answer_from_docs |
ক্লায়েন্ট লিস্ট কল (list_tools, list_resources, list_resource_templates, list_prompts) দিয়ে তিনটাই খুঁজে পায় (discover), সার্ভারের সোর্স কোড জানার দরকার হয় না। এভাবে পাওয়া আউটপুট অন্য পক্ষের ডেটা: পড়ুন, কিন্তু বিশ্বাস করবেন না।
বর্তমান প্রটোকল ডকুমেন্টেশন পড়ুন
প্রটোকল-ভার্সন নেগোশিয়েশন ও সামঞ্জস্যের শর্ত চিহ্নিত করুন। বেশিরভাগ লেখা ও অনেক উদাহরণ এখনো ২০২৪-২০২৫ সালের প্রটোকল বর্ণনা করে: initialize হ্যান্ডশেক, সেশন, স্যাম্পলিং ও রুটস। সেগুলো কপি করবেন না। স্পেসিফিকেশন নিজে পড়ুন, আর ধরে নিন সেটা বদলাবে।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): বর্তমান স্পেসিফিকেশন রিভিশন 2026-07-28; ভার্সন আইডি হলো তারিখ। এই রিভিশনে কোনো
initializeহ্যান্ডশেক নেই এবং প্রটোকল-স্তরের কোনো সেশন নেই: প্রতিটা রিকোয়েস্ট নিজের প্রটোকল ভার্সন বহন করে (রিকোয়েস্টের_meta-তে, আর HTTP-তেMCP-Protocol-Versionহেডারে)। সার্ভার সেই ভার্সন সমর্থন না করলে সেUnsupportedProtocolVersionError(কোড -32022) পাঠায়, যাতে সমর্থিত ভার্সনের তালিকা থাকে, আর ক্লায়েন্ট দুই পক্ষেরই সমর্থিত একটা ভার্সন দিয়ে আবার চেষ্টা করতে পারে। সার্ভারকেserver/discover(ভার্সন, ক্ষমতা, পরিচয়) বাস্তবায়ন করতে হয়;initializeশুধু পুরোনো ("legacy") রিভিশনের জন্য টিকে আছে। স্যাম্পলিং, রুটস ও লগিং Deprecated চিহ্নিত, আর পুরোনো HTTP+SSE ট্রান্সপোর্টও Deprecated; দুটো স্ট্যান্ডার্ড ট্রান্সপোর্ট হলো stdio ও Streamable HTTP। Python SDKmcp২০২৬-১০-০২ তারিখে ছিল 2.3.0 (1.x লাইনে তখনো প্যাচ আসছিল); এর 2.x সার্ভার ক্লাসMCPServer(1.x-এFastMCP) এবং 2.x-এ একটাClientক্লাস আছে। ন্যূনতম Python ভার্সন নিয়ে এর পাতাগুলো পরস্পরবিরোধী: 3.11 বা তার পরের ভার্সন ব্যবহার করুন (এই কোর্স 3.12 ব্যবহার করে)। VERIFY — ২০২৬-১০-১২ তারিখে যাচাই করা; বর্তমান ডকুমেন্টেশন দেখে নিন।
নিরাপত্তা নোট (২০২৬-১০-১২ অনুযায়ী): SDK রিপোজিটরিতে আগের ১২ মাসে প্রকাশিত দশটা নিরাপত্তা অ্যাডভাইজরি তালিকাভুক্ত ছিল (HTTP ট্রান্সপোর্ট, টোকেন হ্যান্ডলিং, রিডাইরেক্ট, সেশন ইত্যাদি)। সবচেয়ে নতুন ফিক্স এসেছে 2.2.0 ও 1.30.0-তে; এই পাতা শুধু stdio ও ইন-মেমরি সংযোগ ব্যবহার করে। তালিকার চেয়ে অভ্যাসটা জরুরি: ফিক্স-সহ (প্যাচ-করা) একটা ভার্সন পিন করুন, প্রতিটা আপগ্রেডের আগে অ্যাডভাইজরি ও রিলিজ নোট পড়ুন, আর রিলিজের সময় আবার দেখুন। VERIFY — ২০২৬-১০-১২ তারিখে যাচাই করা; বর্তমান ডকুমেন্টেশন দেখে নিন।
এই অভ্যাসকে কোডে রূপ দিন। versions.py আমাদের পরীক্ষা করা SDK ও প্রটোকল ভার্সন লিখে রাখে এবং কোনো পার্থক্য জানায়। নিচের ফাইলগুলো kit/evidencedesk/mcp_local/-এর ভেতরে বানান (প্যাকেজ মার্কার __init__.py শুধু একটা ডকস্ট্রিং, সেটা কিটে আছে)।
# not run here: installs a package (already present in the author's environment)
pip install "mcp==2.3.0"# file: kit/evidencedesk/mcp_local/versions.py
"""Pinned versions and a drift check. Re-read the changelog, then change the pins on purpose."""
from __future__ import annotations
from importlib import metadata
EXPECTED_SDK = "2.3.0" # VERIFY — checked 2026-10-12: python-sdk releases and advisories change often
EXPECTED_PROTOCOL = "2026-07-28" # VERIFY — checked 2026-10-12: the spec version this course was written against
def installed_sdk() -> str:
return metadata.version("mcp")
def drift(sdk: str, protocol: str) -> list[str]:
"""What differs from the pins. An empty list means: same versions as the ones we tested."""
found = []
if sdk != EXPECTED_SDK:
found.append(f"sdk {sdk} != pinned {EXPECTED_SDK}")
if protocol != EXPECTED_PROTOCOL:
found.append(f"protocol {protocol} != pinned {EXPECTED_PROTOCOL}")
return foundএকটা টেস্ট ইনস্টল করা SDK ও লাইভ ক্লায়েন্টের জানানো ভার্সন দিয়ে drift() কল করে, আর কোনোটা সরে গেলে ইচ্ছাকৃতভাবে ফেল করে: চেঞ্জলগ ও অ্যাডভাইজরি পড়ুন, তারপর সচেতনভাবে পিন বদলান।
একটা ন্যূনতম লোকাল MCP সার্ভার বানান
ফিক্সচার ডেটা দিয়ে নিরীহ রিড-অনলি টুল প্রকাশ করুন। সার্ভার আপনার আগে থেকে থাকা রেজিস্ট্রিকে মুড়ে দেয়। lookup_document হলো বিগিনার টুল, অপরিবর্তিত; search_help হলো এমবেডিং ও সাদৃশ্য পাতার খেলনা ইনডেক্সের ওপর একটা নতুন রিড-অনলি টুল। দুটোই ToolRegistry.execute কল করে, তাই আর্গুমেন্ট যাচাই, এরর কোড ও "এররে অভ্যন্তরীণ তথ্য নয়" নিয়ম এক জায়গাতেই থাকে। ডিজাইন নোট:
- গঠনেই ন্যূনতম অধিকার।
build_server(tools=...)শুধু আপনার চাওয়া নামগুলো রেজিস্টার করে, এবং শুধুTOOL_NAMES-এর নাম। ফাইলে চালু করার মতো কোনো রাইট টুল নেই, তাই মডেলের সেদিকে যাওয়ার কোনো পথ নেই। রিসোর্স শুধু কর্পাস ফোল্ডারে পাওয়া আইডির মধ্যে সীমিত; ব্যবহারকারীর টেক্সট থেকে বানানো কোনো পাথ কখনো নয়। - অ্যানোটেশন হিন্ট, নিরাপত্তা নয়। স্পেসিফিকেশন বলে, বিশ্বস্ত সার্ভার থেকে না এলে ক্লায়েন্টকে অ্যানোটেশন অবিশ্বস্ত ধরতে হবে; SDK ডকুমেন্টেশন এগুলোকে হিন্ট বলে। এই সার্ভার রিড-অনলি থাকে কারণ এর কোড কখনো লেখে না।
- এররে নিরাপদ কোড ও বার্তা। SDK অপ্রত্যাশিত এক্সসেপশনের টেক্সট লুকিয়ে রাখে; আমরা তার টুল এরর তুলি শুধু আমাদের
code: messageদিয়ে।
# file: kit/evidencedesk/mcp_local/server.py
"""A minimal, read-only MCP server over the synthetic Kestrel help centre. Its tools wrap the Beginner ToolRegistry."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Annotated, Iterable
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ResourceError, ToolError as McpToolError
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field
from evidencedesk.embeddings import LocalSearch, load_corpus
from evidencedesk.tools import Tool, ToolArgs, ToolCall, ToolRegistry, default_registry, read_front_matter
KIT = Path(__file__).resolve().parents[2]
CORPUS = KIT / "corpus"
SERVER_NAME = "kestrel-help"
TOOL_NAMES = ("lookup_document", "search_help") # all read-only; there is no write tool to enable
DOC_ID = r"^[a-z0-9-]{3,40}$"
INSTRUCTIONS = "Read-only Kestrel Cycles help centre (synthetic). No tool changes any data."
HINTS = ToolAnnotations(read_only_hint=True, idempotent_hint=True, open_world_hint=False) # hints, not security
class DocInfo(BaseModel):
id: str
title: str
version: str
status: str
lang: str
class Hit(BaseModel):
passage_id: str
doc_id: str
status: str
score: float
class SearchResult(BaseModel):
query: str
hits: list[Hit]
class SearchArgs(ToolArgs):
query: str = Field(min_length=1, max_length=200)
k: int = Field(default=3, ge=1, le=5)
def help_registry(corpus_dir: Path = CORPUS) -> ToolRegistry:
"""The internal registry: the Beginner lookup tool plus one search tool."""
registry = default_registry(corpus_dir)
index = LocalSearch(load_corpus(corpus_dir))
def search(args: SearchArgs) -> dict:
hits = [Hit(passage_id=h.passage_id, doc_id=index.passages[h.passage_id].doc_id,
status=index.passages[h.passage_id].status, score=round(float(h.score), 3))
for h in index.search(args.query, args.k)]
return SearchResult(query=args.query, hits=hits).model_dump()
registry.register(Tool("search_help", "Search the help centre; returns passage ids with scores, not text.", SearchArgs, search))
return registry
def build_server(corpus_dir: Path = CORPUS, *, tools: Iterable[str] = TOOL_NAMES) -> MCPServer:
"""Least privilege: register only the tools asked for, and only names from TOOL_NAMES."""
wanted = set(tools)
if wanted - set(TOOL_NAMES):
raise ValueError(f"unknown tool names: {sorted(wanted - set(TOOL_NAMES))}")
registry = help_registry(corpus_dir)
ids = {read_front_matter(p)["id"]: p for p in sorted(corpus_dir.glob("*.md"))}
server = MCPServer(SERVER_NAME, instructions=INSTRUCTIONS)
def run(name: str, arguments: dict) -> dict:
result = registry.execute(ToolCall("mcp", name, arguments))
if not result.ok:
raise McpToolError(f"{result.error['code']}: {result.error['message']}") # safe text only; the SDK hides other exceptions
return result.data
if "lookup_document" in wanted:
@server.tool(annotations=HINTS)
def lookup_document(doc_id: Annotated[str, Field(pattern=DOC_ID)]) -> DocInfo:
"""Look up a help-centre document by its id."""
return DocInfo(**run("lookup_document", {"doc_id": doc_id}))
if "search_help" in wanted:
@server.tool(annotations=HINTS)
def search_help(query: Annotated[str, Field(min_length=1, max_length=200)], k: Annotated[int, Field(ge=1, le=5)] = 3) -> SearchResult:
"""Search the help centre; returns passage ids with scores, not text."""
return SearchResult(**run("search_help", {"query": query, "k": k}))
@server.resource("kestrel://index", mime_type="application/json")
def index() -> str:
"""Ids and titles of every document."""
return json.dumps([{"id": i, "title": read_front_matter(p)["title"]} for i, p in sorted(ids.items())], ensure_ascii=False)
@server.resource("kestrel://docs/{doc_id}", mime_type="text/markdown")
def document(doc_id: str) -> str:
"""One document. Only ids found in the corpus folder are readable."""
if doc_id not in ids:
raise ResourceError("no document has that id")
return ids[doc_id].read_text(encoding="utf-8")
@server.prompt()
def answer_from_docs(question: str) -> str:
"""A template the user picks."""
return ("Answer the question using only the Kestrel help-centre documents. Cite document ids. "
f"If they do not answer it, say so.\n\nQuestion: {question}")
return serverএই এন্ট্রি পয়েন্ট সার্ভারটাকে stdio সার্ভার হিসেবে চালায়। স্ট্যান্ডার্ড আউটপুটই এখানে যোগাযোগের তার (wire), তাই একটা বাড়তি print() প্রটোকল নষ্ট করে দেয়: স্ট্যান্ডার্ড এররে লগ করুন। stdio স্যান্ডবক্সও নয়: চাইল্ড প্রসেস, যে সেটা চালু করেছে তার সমান অধিকার নিয়ে চলে।
# file: kit/evidencedesk/mcp_local/__main__.py
"""Run the server over stdio: python -m evidencedesk.mcp_local (stdout is the wire: never print here)."""
from evidencedesk.mcp_local.server import build_server
if __name__ == "__main__":
build_server().run() # no argument = stdioexport PYTHONPATH=kitএবার দেখুন সার্ভার কী কী দেয়। লুপটা দুই মোডে সংযোগ করে এবং প্রতিটা যে ভার্সন জানায় তা ছাপে; বাকিটা লিস্ট কলগুলোর ফেরত দেওয়া তথ্য ছাপে। (SDK ব্যর্থ কল স্ট্যান্ডার্ড এররে লগ করে; ব্লকটা তা নীরব করে দেয়।)
import logging
import anyio
from mcp import Client
from evidencedesk.mcp_local.server import build_server
logging.getLogger("mcp").setLevel(logging.CRITICAL) # the SDK logs failed tool calls to stderr
async def main() -> None:
for mode in ("auto", "legacy"):
async with Client(build_server(), mode=mode) as client:
print(f"mode={mode:6} protocol={client.protocol_version} server={client.server_info.name}")
async with Client(build_server()) as client:
caps = client.server_capabilities
print("capabilities:", [n for n in ("tools", "resources", "prompts", "completions") if getattr(caps, n)])
tools = (await client.list_tools()).tools
print("tools:", [(t.name, t.annotations.read_only_hint) for t in tools])
print("resources:", [str(r.uri) for r in (await client.list_resources()).resources])
print("templates:", [t.uri_template for t in (await client.list_resource_templates()).resource_templates])
print("prompts:", [p.name for p in (await client.list_prompts()).prompts])
anyio.run(main)mode=auto protocol=2026-07-28 server=kestrel-help
mode=legacy protocol=2025-11-25 server=kestrel-help
capabilities: ['tools', 'resources', 'prompts']
tools: [('lookup_document', True), ('search_help', True)]
resources: ['kestrel://index']
templates: ['kestrel://docs/{doc_id}']
prompts: ['answer_from_docs']লক্ষ্য করুন। mode="auto" (ডিফল্ট) 2026-07-28 রিভিশন ব্যবহার করেছে। mode="legacy" নেগোশিয়েট করেছে 2025-11-25, অর্থাৎ পুরোনো সার্ভারের জন্য পুরোনো হ্যান্ডশেক এখনো আছে। দুটো স্ট্রিংই এই SDK ভার্সনের ছাপা আউটপুট, অন্য ভার্সনে বদলাতে পারে। এবার প্রতিটা ধরনের একটা করে কল:
import json
import anyio
from mcp import Client
from evidencedesk.mcp_local.server import build_server
async def main() -> None:
async with Client(build_server()) as client:
called = await client.call_tool("lookup_document", {"doc_id": "kc-recalls-v1"})
print("tool :", called.structured_content)
index = json.loads((await client.read_resource("kestrel://index")).contents[0].text)
print("resource:", len(index), "documents, first:", index[0])
doc = (await client.read_resource("kestrel://docs/kc-recalls-v1")).contents[0].text
print("resource:", doc.splitlines()[0:2], "...", len(doc), "characters")
got = await client.get_prompt("answer_from_docs", {"question": "Is my bike recalled?"})
print("prompt :", got.messages[0].role, "|", got.messages[0].content.text.splitlines()[-1])
anyio.run(main)tool : {'id': 'kc-recalls-v1', 'title': 'Safety recalls', 'version': '1', 'status': 'current', 'lang': 'en'}
resource: 13 documents, first: {'id': 'kc-assembly-v1', 'title': 'Assembling your bike'}
resource: ['---', 'id: kc-recalls-v1'] ... 467 characters
prompt : user | Question: Is my bike recalled?টুল একটা কাঠামোবদ্ধ ডিকশনারি ফেরত দিয়েছে, রিসোর্স অ্যাপ্লিকেশনের কনটেক্সট দিয়েছে, আর প্রম্পট দিয়েছে তৈরি করা একটা ব্যবহারকারী বার্তা। হোস্ট কনটেক্সটে না দিলে মডেল এর কিছুই দেখে না, আর তা দিতে হয় কনটেক্সট সাজানো ও বিশ্বাসের সীমানা পাতার বিশ্বাস-নিয়মে।
টুলের ইনপুট ও আউটপুট যাচাই করুন
স্কিমাকে চুক্তি হিসেবে দেখুন, তবু রানটাইম চেক লাগবে। সার্ভার প্রতিটা টুলের জন্য JSON স্কিমা প্রকাশ করে, আর SDK টাইপ ও প্যাটার্ন যাচাই করে। কিন্তু স্কিমা কেবল একটা প্রতিশ্রুতি। কাঁচা SDK কল এমন ফিল্ড মেনে নেয় যা স্কিমায় ছিলই না, আর ড্রিফট করা সার্ভার এমন ফলাফল দিতে পারে যা তার নিজের প্রচারিত স্কিমা ভাঙে। নিচের ব্রিজ আমাদের দিক দুইবার রক্ষা করে: পাঠানোর আগে বিগিনার Pydantic মডেল (যা বাড়তি ফিল্ড নিষিদ্ধ করে) দিয়ে আর্গুমেন্ট যাচাই করে, আর কাঠামোবদ্ধ ফলাফল যাচাই করে একটা Contract দিয়ে, যা থাকে আমাদের কোডে, সার্ভারের বর্ণনায় নয়।
একটা ন্যূনতম MCP ক্লায়েন্ট বানান ও রেজিস্ট্রিতে ম্যাপ করুন
ডকুমেন্টেশনে বর্ণিত SDK দিয়ে লোকাল টুল খুঁজে বের করুন ও কল করুন, এবং MCP টুলকে অভ্যন্তরীণ টুল রেজিস্ট্রিতে ম্যাপ করুন। McpRegistry-এর ইন্টারফেস ToolRegistry-এর মতো (names, specs, execute), কিন্তু এটা async, শুধু চুক্তি থাকা টুলই কল করা যায়, আর প্রতিটা ফলাফল টুল চুক্তি ও এক্সিকিউশন পাতার একটা ToolResult। অ্যাপ্লিকেশন কখনো প্রটোকলের ফলাফল অবজেক্ট দেখে না। ব্যর্থতার কোড আমাদের নিজস্ব: invalid_arguments, unknown_tool, tool_error (সার্ভার তাই বলেছে; টেক্সট ছাঁটা, কারণ তা অবিশ্বস্ত), bad_result, timeout, protocol_error। চুক্তি ছাড়া সার্ভার টুল ignored-এ তালিকাভুক্ত হয় এবং কখনো কল করা যায় না।
# file: kit/evidencedesk/mcp_local/bridge.py
"""Map MCP tools into the internal tool registry: the application sees ToolCall and ToolResult, never an MCP result object."""
from __future__ import annotations
import sys
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import Any, AsyncIterator
import anyio
from mcp import Client, StdioServerParameters
from mcp.shared.exceptions import MCPError
from pydantic import BaseModel, ValidationError
from evidencedesk.mcp_local.server import KIT, DocInfo, SearchArgs, SearchResult
from evidencedesk.permissions import fail
from evidencedesk.tools import LookupArgs, ToolArgs, ToolCall, ToolResult
CLIP = 200 # server error text is untrusted: clip it
@dataclass(frozen=True)
class Contract:
"""What we expect of a remote tool, written in our code, not taken from the server."""
args: type[ToolArgs]
result: type[BaseModel]
CONTRACTS = {"lookup_document": Contract(LookupArgs, DocInfo), "search_help": Contract(SearchArgs, SearchResult)}
def problems_of(exc: ValidationError) -> list[dict[str, str]]:
return [{"loc": ".".join(str(p) for p in e["loc"]), "msg": e["msg"]} for e in exc.errors()]
class McpRegistry:
"""Like ToolRegistry (names, specs, execute) but async, with the work behind a protocol boundary."""
def __init__(self, client: Client, contracts: dict[str, Contract] = CONTRACTS, timeout: float = 2.0) -> None:
self.client, self.contracts, self.timeout = client, contracts, timeout
self._specs: dict[str, dict[str, Any]] = {}
self.ignored: list[str] = [] # server tools without a contract: listed, never callable
async def refresh(self) -> list[str]:
listing = await self.client.list_tools()
self._specs = {t.name: {"name": t.name, "description": t.description or "", "parameters": t.input_schema}
for t in listing.tools if t.name in self.contracts}
self.ignored = sorted(t.name for t in listing.tools if t.name not in self.contracts)
return self.names()
def names(self) -> list[str]:
return sorted(self._specs)
def specs(self) -> list[dict[str, Any]]:
return [self._specs[n] for n in self.names()]
async def execute(self, call: ToolCall) -> ToolResult:
contract = self.contracts.get(call.name)
if contract is None or call.name not in self._specs:
return fail(call, "unknown_tool", "no such tool is registered")
try:
args = contract.args.model_validate(call.arguments) # our check first: the SDK ignores extra fields
except ValidationError as exc:
return fail(call, "invalid_arguments", "arguments rejected", problems=problems_of(exc))
try:
with anyio.fail_after(self.timeout):
reply = await self.client.call_tool(call.name, args.model_dump())
except TimeoutError:
return fail(call, "timeout", f"tool did not finish within {self.timeout} s")
except MCPError:
return fail(call, "protocol_error", "the server answered with a protocol error")
except Exception: # never leak the text
return fail(call, "internal_error", "the call failed")
if reply.is_error:
text = next((c.text for c in reply.content if getattr(c, "type", "") == "text"), "tool error")
return fail(call, "tool_error", text.splitlines()[0][:CLIP] if text else "tool error")
try:
data = contract.result.model_validate(reply.structured_content).model_dump()
except ValidationError:
return fail(call, "bad_result", "the result does not match the contract")
return ToolResult(call.id, call.name, True, data=data)
def stdio_params() -> StdioServerParameters:
"""Launch `python -m evidencedesk.mcp_local` with this interpreter."""
return StdioServerParameters(command=sys.executable, args=["-m", "evidencedesk.mcp_local"],
cwd=str(KIT), env={"PYTHONPATH": str(KIT)})
@asynccontextmanager
async def open_registry(target: Any, **options: Any) -> AsyncIterator[McpRegistry]:
"""Connect to a server object (in memory) or StdioServerParameters, discover its tools, yield the registry."""
async with Client(target) as client:
registry = McpRegistry(client, **options)
await registry.refresh()
yield registryআসল SDK-তে তিনটা ফাঁক চালিয়ে দেখুন। দ্বিতীয় সার্ভারটা ইচ্ছাকৃতভাবে ড্রিফট করা একটা স্ট্যান্ড-ইন, যা যেখানে স্ট্রিং আশা করি সেখানে সংখ্যা ফেরত দেয়।
import logging
import anyio
from mcp import Client
from mcp.server import MCPServer
from evidencedesk.mcp_local.bridge import open_registry
from evidencedesk.mcp_local.server import build_server
from evidencedesk.tools import ToolCall
logging.getLogger("mcp").setLevel(logging.CRITICAL)
sneaky = {"doc_id": "kc-returns-v3", "role": "admin"}
rogue = MCPServer("rogue") # a server whose result drifted away from our contract
@rogue.tool()
def lookup_document(doc_id: str) -> dict[str, int]:
return {"id": 7}
async def main() -> None:
async with Client(build_server()) as client: # the raw SDK call
raw = await client.call_tool("lookup_document", sneaky)
print("raw SDK, extra field : is_error =", raw.is_error)
async with open_registry(build_server()) as registry: # our bridge
result = await registry.execute(ToolCall("c1", "lookup_document", sneaky))
print("bridge, extra field :", result.error["code"], result.error["problems"][0])
async with open_registry(rogue) as registry:
result = await registry.execute(ToolCall("c2", "lookup_document", {"doc_id": "kc-returns-v3"}))
print("bridge, drifted result :", result.error)
anyio.run(main)raw SDK, extra field : is_error = False
bridge, extra field : invalid_arguments {'loc': 'role', 'msg': 'Extra inputs are not permitted'}
bridge, drifted result : {'code': 'bad_result', 'message': 'the result does not match the contract'}কাঁচা SDK বাড়তি role ফিল্ড মেনে নিয়েছে। আমাদের ব্রিজ পাঠানোর আগেই তা প্রত্যাখ্যান করেছে, আর ড্রিফট করা ফলাফলকে পরিষ্কার bad_result বানিয়েছে, ভুল আকার সামনে ছড়াতে দেয়নি। MCP প্রমাণীকরণ, অথরাইজেশন বা যাচাইয়ের বিকল্প নয়।
LLM ছাড়াই MCP প্রবাহ টেস্ট করুন
সাধারণ Python টেস্ট দিয়ে সার্ভার কল করে ফলাফল যাচাই করুন। টুল টেস্ট করতে মডেল লাগে না। SDK-তে সার্ভার অবজেক্টকে Client-এ দিলে সংযোগ ইন-মেমরিতে হয়: সাবপ্রসেস নেই, পোর্ট নেই। কিটে তিনটা টেস্ট ফাইল আছে (test_mcp_local_server.py, test_mcp_local_bridge.py, test_mcp_local_roundtrip.py), যা আরও অনেক কিছুর সঙ্গে এগুলো যাচাই করে:
- টুলের তালিকা ঠিক দুটো রিড-অনলি টুল; উপসেট কাজ করে ও অজানা নাম প্রত্যাখ্যাত হয়; সার্ভারের স্কিমা রেজিস্ট্রির স্কিমার সঙ্গে মেলে (র্যাপার নিজের সিগনেচার ঘোষণা করে, তাই ড্রিফট ধরতে টেস্ট লাগে);
- শত্রুভাবাপন্ন রিসোর্স আইডি প্রত্যাখ্যাত হয় এবং এররে কোনো পাথ থাকে না;
- শত্রুভাবাপন্ন ইন-মেমরি সার্ভারের বিরুদ্ধে: অবৈধ আর্গুমেন্ট কখনো সার্ভারে পৌঁছায় না, চুক্তি ছাড়া টুল চলতে পারে না, ৮,৫০০ অক্ষরের এরর ছাঁটা হয়, ভাঙা ফলাফল
bad_result, ধীর টুল টাইমআউট হয়; - একটা টেস্ট সার্ভারকে আসল stdio চাইল্ড প্রসেস হিসেবে চালায় এবং ইন-মেমরি রানের একই উত্তর দাবি করে (একটা লোকাল প্রসেস, নেটওয়ার্ক নেই, নির্ধারিত)। বাকি ১৯টা টেস্ট শুধু ইন-মেমরি।
টেস্ট ফাইলগুলো mcp ও anyio importlib দিয়ে নাম ধরে ইমপোর্ট করে এবং mcp ইনস্টল না থাকলে নিজেরা স্কিপ হয়। কারণ: প্যাকেজিং, কনফিগারেশন ও পুনরুৎপাদনযোগ্যতা পাতার স্যুট যাচাই করে যে টেস্ট শুধু ঘোষিত প্যাকেজ ইমপোর্ট করে, আর mcp ঐচ্ছিক।
দুই পথের তুলনা
পাঁচটা কল চলে সরাসরি রেজিস্ট্রি দিয়ে, ইন-মেমরি ব্রিজ দিয়ে, এবং stdio চাইল্ড প্রসেসের ওপর ব্রিজ দিয়ে; শেষ কলটা এমন টুল চায় যা রেজিস্টার করা নেই। (stdio চাইল্ড স্ট্যান্ডার্ড এররে একটা লগ লাইন লেখে; আমি ব্লকটা 2>/dev/null দিয়ে চালিয়েছি, তাই শুধু স্ট্যান্ডার্ড আউটপুট দেখানো হয়েছে।)
import logging
import anyio
from evidencedesk.mcp_local.bridge import open_registry, stdio_params
from evidencedesk.mcp_local.server import CORPUS, build_server, help_registry
from evidencedesk.tools import ToolCall
logging.getLogger("mcp").setLevel(logging.CRITICAL)
CALLS = [ToolCall("a", "lookup_document", {"doc_id": "kc-returns-v3"}),
ToolCall("b", "search_help", {"query": "battery charging", "k": 1}),
ToolCall("c", "lookup_document", {"doc_id": "no-such-doc"}),
ToolCall("d", "search_help", {"query": ""}),
ToolCall("e", "send_email", {"to": "a@example.org"})]
def show(label: str, results) -> None:
print(f"{label:10}", " ".join(f"{r.call_id}={'ok' if r.ok else r.error['code']}" for r in results))
async def main() -> None:
direct = help_registry(CORPUS)
show("direct", [direct.execute(c) for c in CALLS])
async with open_registry(build_server()) as registry:
in_memory = [await registry.execute(c) for c in CALLS]
show("in-memory", in_memory)
async with open_registry(stdio_params()) as registry:
over_stdio = [await registry.execute(c) for c in CALLS]
show("stdio", over_stdio)
print("same results in memory and over stdio:", in_memory == over_stdio)
print("same data as direct for ok calls:", all(d.data == m.data for d, m in zip((direct.execute(c) for c in CALLS), in_memory) if d.ok))
anyio.run(main)direct a=ok b=ok c=not_found d=invalid_arguments e=unknown_tool
in-memory a=ok b=ok c=tool_error d=invalid_arguments e=unknown_tool
stdio a=ok b=ok c=tool_error d=invalid_arguments e=unknown_tool
same results in memory and over stdio: True
same data as direct for ok calls: Trueসফল কলগুলোর ডেটা তিন পথেই অভিন্ন। কিন্তু c কল সীমানার দাম দেখায়: সরাসরি রেজিস্ট্রি বলে not_found, আর MCP-তে একই ব্যর্থতা আসে সাধারণ tool_error হয়ে, নির্দিষ্ট কোড শুধু বার্তার টেক্সটের ভেতরে। সেই টেক্সট থেকে কোড বের করা যেত, কিন্তু টেক্সট আসে অন্য পক্ষ থেকে, তাই আমরা তা ছাঁটা টেক্সট হিসেবে রাখি আর সিদ্ধান্ত নিই নিজেদের কোডে। সীমানা এমন ব্যর্থতাও যোগ করেছে যা প্রসেসের ভেতরে হতে পারে না (bad_result, timeout, protocol_error): তুলনার টেবিলের "নতুন ব্যর্থতা" সারি, চলমান কোডে।
অনুশীলন: লোকাল MCP রাউন্ড ট্রিপ
একটা ফিক্সচার সার্ভার চালু করুন, ক্লায়েন্ট থেকে কল করুন এবং নির্ধারিত আউটপুট যাচাই করুন। এই ছোট ফাইলটাই অনুশীলন; এটা বাকি দুটো টেস্ট ফাইলের সঙ্গে চালান।
# file: kit/tests/test_mcp_local_roundtrip.py
"""Exercise: a local MCP round trip. Start the fixture server in memory, call it through the bridge, check deterministic output."""
import importlib
import pytest
pytest.importorskip("mcp") # skipped when the optional `mcp` package is not installed
anyio = importlib.import_module("anyio") # loaded by name: test_api scans tests/*.py for undeclared third-party imports
from evidencedesk.mcp_local.bridge import open_registry # noqa: E402
from evidencedesk.mcp_local.server import build_server # noqa: E402
from evidencedesk.tools import ToolCall # noqa: E402
RETURNS = {"id": "kc-returns-v3", "title": "Returns policy", "version": "3", "status": "current", "lang": "en"}
def round_trip(name, arguments):
async def main():
async with open_registry(build_server()) as registry:
return registry.names(), await registry.execute(ToolCall("t1", name, arguments))
return anyio.run(main)
def test_lookup_round_trip_is_deterministic():
_, first = round_trip("lookup_document", {"doc_id": "kc-returns-v3"})
assert first.ok and first.data == RETURNS
assert round_trip("lookup_document", {"doc_id": "kc-returns-v3"})[1] == first # same call, same answer
def test_an_unknown_document_is_a_clean_failure():
_, result = round_trip("lookup_document", {"doc_id": "no-such-doc"})
assert not result.ok and result.error["code"] == "tool_error" and "not_found" in result.error["message"]
def test_the_model_can_reach_no_write_tool():
names, result = round_trip("send_email", {"to": "a@example.org"})
assert names == ["lookup_document", "search_help"]
assert result.error["code"] == "unknown_tool"cd kit
python -m pytest tests/test_mcp_local_server.py tests/test_mcp_local_bridge.py tests/test_mcp_local_roundtrip.py.................... [100%]
20 passed(গণনার পরে pytest যে পেরিয়ে-যাওয়া সেকেন্ড ছাপে, এখানে তা বাদ দেওয়া হয়েছে।) তৃতীয় টেস্টটা মনে রাখুন: মডেল রাইট টুলে পৌঁছাতে পারে না, কারণ তা নেই; send_email চাইলে unknown_tool ফেরত আসে।
কাজের জায়গায় এটা কোথায় দেখা যায়
প্ল্যাটফর্ম টিমগুলো একটা অভ্যন্তরীণ টুল সার্ভার প্রকাশ করে (টিকিট লুকআপ, প্রোডাক্ট ক্যাটালগ, রানবুক সার্চ), যাতে সাপোর্ট বট, সেলস অ্যাসিস্ট্যান্ট ও ইঞ্জিনিয়ারদের এডিটর একই অডিট করা কোড ব্যবহার করে। রিভিউয়ের প্রশ্ন সবসময় একই: এই সার্ভার কী করতে পারে, কে কল করতে পারে, আর আমাদের অজান্তে বদলে গেলে কী হবে? পিন করা ভার্সন, কলারের রিপোজিটরিতে চুক্তি এবং মডেল ছাড়া চলা টেস্ট এই উত্তরগুলো ছোট করে দেয়।
সাধারণ ভুল
- ২০২৫ সালের টিউটোরিয়াল কপি করা। তা হ্যান্ডশেক ও সেশন শেখায়। বর্তমান স্পেসিফিকেশন পড়ুন, SDK ভার্সন পিন করুন, ড্রিফট টেস্ট যোগ করুন।
- stdio সার্ভারে
print()ব্যবহার। স্ট্যান্ডার্ড আউটপুট প্রটোকলের চ্যানেল, তাই একটা বাড়তি লাইন সব ক্লায়েন্ট ভেঙে দেয়। স্ট্যান্ডার্ড এররে লগ করুন। - স্কিমা বা অ্যানোটেশন বিশ্বাস করা। SDK একটা অজানা ফিল্ড উপেক্ষা করেছে, আর
read_only_hintএকটা হিন্ট। দুই দিকেই যাচাই করুন, আর রিড-অনলি হওয়াটা কোডেই নিশ্চিত করুন। - প্রটোকল অবজেক্ট অ্যাপ্লিকেশনের ভেতর দিয়ে ছড়ানো। তখন প্রতিটা কলারকে MCP জানতে হয়। সীমানাতেই নিজের ফলাফল টাইপে রূপান্তর করুন, আর সাধারণ কল দিয়ে (মডেল ছাড়া) টেস্ট করুন, শত্রুভাবাপন্ন সার্ভারের কেসসহ।
নিজে চেষ্টা করুন
- সহজ। টুল, রিসোর্স ও প্রম্পটের প্রতিটার জন্য বলুন কে নিয়ন্ত্রণ করে এবং আমাদের সার্ভার থেকে একটা করে উদাহরণ দিন। টুল র্যাপার কোন বিগিনার পাতার রেজিস্ট্রি পুনর্ব্যবহার করে?
- মাঝারি। সার্ভার বানান শুধু
lookup_documentদিয়ে, আর ক্লায়েন্টকেsearch_helpকল করতে বলুন। কোন কোড ফেরত আসে, আর টুলের অনুমতি ও ব্যর্থতা সামলানো পাতার কোন ধারণা এটা? - কঠিন। এমন একটা টেস্ট লিখুন যা চারটা শত্রুভাবাপন্ন রিসোর্স URI পড়তে চায় (পাথ ট্রাভার্সাল, নাল বাইট, ভুল অক্ষর-কেসের আইডি, একটা
file:URI) এবং প্রতিটার জন্য সংক্ষিপ্ত প্রত্যাখ্যান দাবি করে।
উত্তর
সহজ। টুল: মডেল, হোস্টের মাধ্যমে (lookup_document)। রিসোর্স: অ্যাপ্লিকেশন (kestrel://docs/{doc_id})। প্রম্পট: ব্যবহারকারী (answer_from_docs)। র্যাপার টুল চুক্তি ও এক্সিকিউশন পাতার রেজিস্ট্রি পুনর্ব্যবহার করে, তাই Pydantic যাচাই ও এরর কোড একই। চেকলিস্ট: তিনটা নিয়ন্ত্রক ঠিকভাবে বলা; প্রতিটার একটা উদাহরণ; সঠিক বিগিনার শিরোনাম।
মাঝারি। চালান:
import anyio
from evidencedesk.mcp_local.bridge import open_registry
from evidencedesk.mcp_local.server import build_server
from evidencedesk.tools import ToolCall
async def main() -> None:
async with open_registry(build_server(tools=["lookup_document"])) as registry:
print(registry.names())
result = await registry.execute(ToolCall("m1", "search_help", {"query": "returns"}))
print(result.error["code"])
anyio.run(main)['lookup_document']
unknown_toolএটা একটা অ্যালাউলিস্ট, সার্ভার তৈরির সময় প্রয়োগ করা (এবং ব্রিজও আবার প্রয়োগ করে, কারণ সার্ভার যে টুল তালিকাভুক্ত করেনি তা কল করা যায় না)। চেকলিস্ট: নামের তালিকায় search_help নেই; এক্সসেপশন নয়, unknown_tool; আপনি বলতে পারেন কেন সার্ভার ও হোস্ট দুজনেরই টুল সীমিত করা উচিত।
কঠিন। এই ফাইলটা সেভ করুন এবং kit/ থেকে python -m pytest tests/test_mcp_local_hostile_uris.py চালান।
# file: kit/tests/test_mcp_local_hostile_uris.py
import importlib
import pytest
pytest.importorskip("mcp")
mcp = importlib.import_module("mcp") # loaded by name: test_api scans tests/*.py for undeclared third-party imports
anyio = importlib.import_module("anyio")
MCPError = importlib.import_module("mcp.shared.exceptions").MCPError
from evidencedesk.mcp_local.server import build_server # noqa: E402
HOSTILE = ["kestrel://docs/..%2F..%2Fetc%2Fpasswd", "kestrel://docs/kc-returns-v3%00.md", "kestrel://docs/KC-RETURNS-V3", "file:///etc/passwd"]
def test_hostile_resource_uris_are_refused():
async def main():
out = []
async with mcp.Client(build_server()) as client:
for uri in HOSTILE:
with pytest.raises(MCPError) as caught:
await client.read_resource(uri)
out.append(str(caught.value))
return out
for message in anyio.run(main):
assert message.startswith(("Unknown resource", "no document")) # a short refusal; the error may echo the URI, never file contents. [100%]
1 passedচেকলিস্ট: প্রতিটা URI-তে SDK-র প্রটোকল এরর উঠেছে; কোনোটাই ফাইলের বিষয়বস্তু ফেরত দেয়নি; প্রত্যাখ্যান সংক্ষিপ্ত; আপনি দেখেছেন অজানা URI ব্যর্থ হয় SDK-র রাউটিংয়ে, আর ভুল অক্ষর-কেসের আইডি আমাদের কোডে পৌঁছে কর্পাস-আইডি অ্যালাউলিস্টে প্রত্যাখ্যাত হয়।
সারসংক্ষেপ
- হোস্ট নীতির মালিক; ক্লায়েন্ট একটা সার্ভারের সঙ্গে কথা বলে; সার্ভার দেয় টুল (মডেল-নিয়ন্ত্রিত), রিসোর্স (অ্যাপ্লিকেশন-নিয়ন্ত্রিত) ও প্রম্পট (ব্যবহারকারী-নিয়ন্ত্রিত)।
- একটা অ্যাপ্লিকেশন টুলের মালিক থাকলে লোকাল রেজিস্ট্রি ব্যবহার করুন। প্রটোকল সীমানা পুনর্ব্যবহার ও আলাদা করার সুবিধা দেয়, বিনিময়ে নতুন ব্যর্থতা ও রক্ষণাবেক্ষণের একটা সার্ভিস আনে।
- বর্তমান স্পেসিফিকেশনে (২০২৬-১০-১২ অনুযায়ী) কোনো হ্যান্ডশেক নেই; এটা পড়ুন, SDK ও প্রটোকল ভার্সন পিন করুন, ড্রিফট টেস্ট করুন, আর আপগ্রেডের আগে অ্যাডভাইজরি আবার দেখুন।
- স্কিমা ও অ্যানোটেশন প্রতিশ্রুতি, চেক নয়: পাঠানোর আগে আর্গুমেন্ট এবং পাওয়ার পরে ফলাফল যাচাই করুন, আর চুক্তি রাখুন নিজের কোডেই।
- MCP টুলকে নিজের রেজিস্ট্রিতে ম্যাপ করুন, যাতে অ্যাপ্লিকেশন শুধু
ToolCallওToolResultদেখে, আর পুরো প্রবাহ মডেল ছাড়া টেস্ট করুন।
এরপর: MCP নিরাপত্তা ও আন্তঃকার্যক্ষমতা এই সার্ভারকে বিশ্বস্ত লোকাল প্রসেস থেকে বাস্তব জগতের দিকে নিয়ে যায়: ট্রান্সপোর্ট, সংযোগের এরর, প্রমাণীকরণ, শত্রুভাবাপন্ন টুল টেক্সট, ভার্সন ড্রিফট, সীমা ও অডিট ইভেন্ট।