Skip to content

Commit a697d46

Browse files
authored
Add files via upload
Added tutorial on "How to Build a Stateless, Secure, and Asynchronous MCP-Style Protocol for Scalable Agent Workflows"
1 parent 1f3b57c commit a697d46

File tree

1 file changed

+287
-0
lines changed

1 file changed

+287
-0
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
{
2+
"nbformat": 4,
3+
"nbformat_minor": 0,
4+
"metadata": {
5+
"colab": {
6+
"provenance": []
7+
},
8+
"kernelspec": {
9+
"name": "python3",
10+
"display_name": "Python 3"
11+
},
12+
"language_info": {
13+
"name": "python"
14+
}
15+
},
16+
"cells": [
17+
{
18+
"cell_type": "code",
19+
"source": [
20+
"import asyncio, time, json, uuid, hmac, hashlib\n",
21+
"from dataclasses import dataclass\n",
22+
"from typing import Any, Dict, Optional, Literal, List\n",
23+
"from pydantic import BaseModel, Field, ValidationError, ConfigDict\n",
24+
"\n",
25+
"def _now_ms():\n",
26+
" return int(time.time() * 1000)\n",
27+
"\n",
28+
"def _uuid():\n",
29+
" return str(uuid.uuid4())\n",
30+
"\n",
31+
"def _canonical_json(obj):\n",
32+
" return json.dumps(obj, separators=(\",\", \":\"), sort_keys=True).encode()\n",
33+
"\n",
34+
"def _hmac_hex(secret, payload):\n",
35+
" return hmac.new(secret, _canonical_json(payload), hashlib.sha256).hexdigest()"
36+
],
37+
"metadata": {
38+
"id": "jd_g-C6kqngD"
39+
},
40+
"execution_count": null,
41+
"outputs": []
42+
},
43+
{
44+
"cell_type": "code",
45+
"source": [
46+
"class MCPEnvelope(BaseModel):\n",
47+
" model_config = ConfigDict(extra=\"forbid\")\n",
48+
" v: Literal[\"mcp/0.1\"] = \"mcp/0.1\"\n",
49+
" request_id: str = Field(default_factory=_uuid)\n",
50+
" ts_ms: int = Field(default_factory=_now_ms)\n",
51+
" client_id: str\n",
52+
" server_id: str\n",
53+
" tool: str\n",
54+
" args: Dict[str, Any] = Field(default_factory=dict)\n",
55+
" nonce: str = Field(default_factory=_uuid)\n",
56+
" signature: str\n",
57+
"\n",
58+
"class MCPResponse(BaseModel):\n",
59+
" model_config = ConfigDict(extra=\"forbid\")\n",
60+
" v: Literal[\"mcp/0.1\"] = \"mcp/0.1\"\n",
61+
" request_id: str\n",
62+
" ts_ms: int = Field(default_factory=_now_ms)\n",
63+
" ok: bool\n",
64+
" server_id: str\n",
65+
" status: Literal[\"ok\", \"accepted\", \"running\", \"done\", \"error\"]\n",
66+
" result: Optional[Dict[str, Any]] = None\n",
67+
" error: Optional[str] = None\n",
68+
" signature: str"
69+
],
70+
"metadata": {
71+
"id": "S_TwMtR5qnWI"
72+
},
73+
"execution_count": null,
74+
"outputs": []
75+
},
76+
{
77+
"cell_type": "code",
78+
"source": [
79+
"class ServerIdentityOut(BaseModel):\n",
80+
" model_config = ConfigDict(extra=\"forbid\")\n",
81+
" server_id: str\n",
82+
" fingerprint: str\n",
83+
" capabilities: Dict[str, Any]\n",
84+
"\n",
85+
"class BatchSumIn(BaseModel):\n",
86+
" model_config = ConfigDict(extra=\"forbid\")\n",
87+
" numbers: List[float] = Field(min_length=1)\n",
88+
"\n",
89+
"class BatchSumOut(BaseModel):\n",
90+
" model_config = ConfigDict(extra=\"forbid\")\n",
91+
" count: int\n",
92+
" total: float\n",
93+
"\n",
94+
"class StartLongTaskIn(BaseModel):\n",
95+
" model_config = ConfigDict(extra=\"forbid\")\n",
96+
" seconds: int = Field(ge=1, le=20)\n",
97+
" payload: Dict[str, Any] = Field(default_factory=dict)\n",
98+
"\n",
99+
"class PollJobIn(BaseModel):\n",
100+
" model_config = ConfigDict(extra=\"forbid\")\n",
101+
" job_id: str"
102+
],
103+
"metadata": {
104+
"id": "fd9g8VQyqnTY"
105+
},
106+
"execution_count": null,
107+
"outputs": []
108+
},
109+
{
110+
"cell_type": "code",
111+
"source": [
112+
"@dataclass\n",
113+
"class JobState:\n",
114+
" job_id: str\n",
115+
" status: str\n",
116+
" result: Optional[Dict[str, Any]] = None\n",
117+
" error: Optional[str] = None\n",
118+
"\n",
119+
"class MCPServer:\n",
120+
" def __init__(self, server_id, secret):\n",
121+
" self.server_id = server_id\n",
122+
" self.secret = secret\n",
123+
" self.jobs = {}\n",
124+
" self.tasks = {}\n",
125+
"\n",
126+
" def _fingerprint(self):\n",
127+
" return hashlib.sha256(self.secret).hexdigest()[:16]\n",
128+
"\n",
129+
" async def handle(self, env_dict, client_secret):\n",
130+
" env = MCPEnvelope(**env_dict)\n",
131+
" payload = env.model_dump()\n",
132+
" sig = payload.pop(\"signature\")\n",
133+
" if _hmac_hex(client_secret, payload) != sig:\n",
134+
" return {\"error\": \"bad signature\"}\n",
135+
"\n",
136+
" if env.tool == \"server_identity\":\n",
137+
" out = ServerIdentityOut(\n",
138+
" server_id=self.server_id,\n",
139+
" fingerprint=self._fingerprint(),\n",
140+
" capabilities={\"async\": True, \"stateless\": True},\n",
141+
" )\n",
142+
" resp = MCPResponse(\n",
143+
" request_id=env.request_id,\n",
144+
" ok=True,\n",
145+
" server_id=self.server_id,\n",
146+
" status=\"ok\",\n",
147+
" result=out.model_dump(),\n",
148+
" signature=\"\",\n",
149+
" )\n",
150+
"\n",
151+
" elif env.tool == \"batch_sum\":\n",
152+
" args = BatchSumIn(**env.args)\n",
153+
" out = BatchSumOut(count=len(args.numbers), total=sum(args.numbers))\n",
154+
" resp = MCPResponse(\n",
155+
" request_id=env.request_id,\n",
156+
" ok=True,\n",
157+
" server_id=self.server_id,\n",
158+
" status=\"ok\",\n",
159+
" result=out.model_dump(),\n",
160+
" signature=\"\",\n",
161+
" )\n",
162+
"\n",
163+
" elif env.tool == \"start_long_task\":\n",
164+
" args = StartLongTaskIn(**env.args)\n",
165+
" jid = _uuid()\n",
166+
" self.jobs[jid] = JobState(jid, \"running\")\n",
167+
"\n",
168+
" async def run():\n",
169+
" await asyncio.sleep(args.seconds)\n",
170+
" self.jobs[jid].status = \"done\"\n",
171+
" self.jobs[jid].result = args.payload\n",
172+
"\n",
173+
" self.tasks[jid] = asyncio.create_task(run())\n",
174+
" resp = MCPResponse(\n",
175+
" request_id=env.request_id,\n",
176+
" ok=True,\n",
177+
" server_id=self.server_id,\n",
178+
" status=\"accepted\",\n",
179+
" result={\"job_id\": jid},\n",
180+
" signature=\"\",\n",
181+
" )\n",
182+
"\n",
183+
" elif env.tool == \"poll_job\":\n",
184+
" args = PollJobIn(**env.args)\n",
185+
" job = self.jobs[args.job_id]\n",
186+
" resp = MCPResponse(\n",
187+
" request_id=env.request_id,\n",
188+
" ok=True,\n",
189+
" server_id=self.server_id,\n",
190+
" status=job.status,\n",
191+
" result=job.result,\n",
192+
" signature=\"\",\n",
193+
" )\n",
194+
"\n",
195+
" payload = resp.model_dump()\n",
196+
" resp.signature = _hmac_hex(self.secret, payload)\n",
197+
" return resp.model_dump()"
198+
],
199+
"metadata": {
200+
"id": "Fap0YgEiqnQs"
201+
},
202+
"execution_count": null,
203+
"outputs": []
204+
},
205+
{
206+
"cell_type": "code",
207+
"execution_count": 11,
208+
"metadata": {
209+
"id": "VE1XPz73I8Zt",
210+
"colab": {
211+
"base_uri": "https://localhost:8080/"
212+
},
213+
"outputId": "b700fa90-bf4c-49d4-bec7-f198acb17151"
214+
},
215+
"outputs": [
216+
{
217+
"output_type": "stream",
218+
"name": "stdout",
219+
"text": [
220+
"=== 1) SERVER IDENTITY (stateless envelope + signed response) ===\n",
221+
"raw: {'v': 'mcp/0.1', 'request_id': '552f2dd3-a2d2-4814-9c9a-de0d18b3a09a', 'ts_ms': 1766383443221, 'ok': True, 'server_id': 'mcp-server-prod-001', 'status': 'ok', 'result': {'server_id': 'mcp-server-prod-001', 'fingerprint': '937b0da37c69a0d4', 'capabilities': {'async_jobs': True, 'stateless_envelopes': True, 'sdk_validation': True, 'tools': ['server_identity', 'start_long_task', 'poll_job', 'batch_sum']}}, 'error': None, 'signature': '34bc71bb1ebac2612e110720338631b896aebdd765f5235dc5b243c4cc1aeb6c'}\n",
222+
"server signature ok?: True\n",
223+
"\n",
224+
"=== 2) SDK VALIDATION (good batch_sum) ===\n",
225+
"raw: {'v': 'mcp/0.1', 'request_id': 'a983e5ed-028b-478e-9ed8-d0423d9abaac', 'ts_ms': 1766383443222, 'ok': True, 'server_id': 'mcp-server-prod-001', 'status': 'ok', 'result': {'count': 4, 'total': 16.5}, 'error': None, 'signature': 'ea1c8c96302a396557cbe69e4c9b274abe361659c8e9d6ee7ce0c099254436e4'}\n",
226+
"server signature ok?: True\n",
227+
"\n",
228+
"=== 3) SDK VALIDATION (bad batch_sum -> error) ===\n",
229+
"raw: {'v': 'mcp/0.1', 'request_id': 'd362ffcc-d153-4d6f-89f9-eb954203c8cd', 'ts_ms': 1766383443222, 'ok': False, 'server_id': 'mcp-server-prod-001', 'status': 'error', 'result': None, 'error': \"Tool args validation error: [{'type': 'too_short', 'loc': ('numbers',), 'msg': 'List should have at least 1 item after validation, not 0', 'input': [], 'ctx': {'field_type': 'List', 'min_length': 1, 'actual_length': 0}, 'url': 'https://errors.pydantic.dev/2.12/v/too_short'}]\", 'signature': '34591661fc91d836d1a3fc143937248b65eb2e0d22e9659a18e76d40855a9799'}\n",
230+
"server signature ok?: True\n",
231+
"\n",
232+
"=== 4) ASYNC LONG-RUNNING OP (start -> poll -> done) ===\n",
233+
"start: {'v': 'mcp/0.1', 'request_id': '90f182e6-cd3e-4cef-acf7-999780255cfb', 'ts_ms': 1766383443222, 'ok': True, 'server_id': 'mcp-server-prod-001', 'status': 'accepted', 'result': {'job_id': '6b40192d-b686-46ea-8bc5-193e5087d20a', 'status': 'running'}, 'error': None, 'signature': '34da5c27cec1a720ec024bb81299f15e5ec6876a94898d14879da38f668ef781'}\n",
234+
"poll: running\n",
235+
"poll: running\n",
236+
"poll: running\n",
237+
"poll: running\n",
238+
"poll: running\n",
239+
"poll: done\n",
240+
"final: {'v': 'mcp/0.1', 'request_id': '0b6c840a-b658-431e-9939-c3badd686af0', 'ts_ms': 1766383446734, 'ok': True, 'server_id': 'mcp-server-prod-001', 'status': 'done', 'result': {'job_id': '6b40192d-b686-46ea-8bc5-193e5087d20a', 'status': 'done', 'result': {'echo_payload': {'job': 'recompute_embeddings', 'scope': 'Q4'}, 'duration_s': 3, 'completed_ts_ms': 1766383446223}, 'error': None}, 'error': None, 'signature': 'ff92745c64f07c002cc4fb07fd71bbae55f338aafb5274c763f44ddd93bb4f5f'}\n",
241+
"\n",
242+
"✅ Demo complete: async ops + stateless envelopes + strict SDK validation.\n"
243+
]
244+
}
245+
],
246+
"source": [
247+
"class MCPClient:\n",
248+
" def __init__(self, client_id, secret, server):\n",
249+
" self.client_id = client_id\n",
250+
" self.secret = secret\n",
251+
" self.server = server\n",
252+
"\n",
253+
" async def call(self, tool, args=None):\n",
254+
" env = MCPEnvelope(\n",
255+
" client_id=self.client_id,\n",
256+
" server_id=self.server.server_id,\n",
257+
" tool=tool,\n",
258+
" args=args or {},\n",
259+
" signature=\"\",\n",
260+
" ).model_dump()\n",
261+
" env[\"signature\"] = _hmac_hex(self.secret, {k: v for k, v in env.items() if k != \"signature\"})\n",
262+
" return await self.server.handle(env, self.secret)\n",
263+
"\n",
264+
"async def demo():\n",
265+
" server_secret = b\"server_secret\"\n",
266+
" client_secret = b\"client_secret\"\n",
267+
" server = MCPServer(\"mcp-server-001\", server_secret)\n",
268+
" client = MCPClient(\"client-001\", client_secret, server)\n",
269+
"\n",
270+
" print(await client.call(\"server_identity\"))\n",
271+
" print(await client.call(\"batch_sum\", {\"numbers\": [1, 2, 3]}))\n",
272+
"\n",
273+
" start = await client.call(\"start_long_task\", {\"seconds\": 2, \"payload\": {\"task\": \"demo\"}})\n",
274+
" jid = start[\"result\"][\"job_id\"]\n",
275+
"\n",
276+
" while True:\n",
277+
" poll = await client.call(\"poll_job\", {\"job_id\": jid})\n",
278+
" if poll[\"status\"] == \"done\":\n",
279+
" print(poll)\n",
280+
" break\n",
281+
" await asyncio.sleep(0.5)\n",
282+
"\n",
283+
"await demo()"
284+
]
285+
}
286+
]
287+
}

0 commit comments

Comments
 (0)