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