Bouw een MCP-client en server die fouten aankunnen
Verbind eigen client- en serverstappen via echte begrensde pakketqueues; meet succes, weigering, annulering en verbindingsfouten.
De opdracht, leerdoelen en voorkennis
- Je verbindt eigen request-, server-, client- en sessiestappen met de gegeven begrensde transporthelpers.
- Je bewaart correlatie en geschiedenis bij fouten en vergelijkt twaalf uitgevoerde sessies op echte pakketten en UTF-8-bytes.
Lees eerst: Begin een MCP-verbinding met heldere afspraken · Geef protocolfouten en toolfouten hun eigen laag · Koppel ieder antwoord aan het juiste verzoek · Voeg workerresultaten pas samen na controle.
Je rekentool kan losse MCP-berichten beantwoorden. Nu maak je er een gesprek van: de client reserveert een ID, de server bewaakt zijn fase en een antwoord wordt pas na controle bruikbaar. Als één kant stopt, moeten ook de andere kant en alle wachtende verzoeken netjes afsluiten.
De host beheert hier een eigen geheugenverbinding met twee wachtrijen. Gegeven helpers verzorgen begrensde UTF-8/JSON, registers en kopieën; jij verbindt de eerder geleerde onderdelen in vijf cellen. Het resultaat is een vaste workflow met echte berekeningen en pakketten, binnen de oefenvorm van MCP-editie 2025-11-25.
1. Reserveer voordat je verstuurt
Denk aan een loket met genummerde ontvangstbewijzen. De client moet weten waarop hij wacht voordat een antwoord kan terugkomen. Schrijf prepare_mcp_request: controleer de clientfase, registreer het getypeerde ID en geef de te versturen request terug.
request = {
'jsonrpc':'2.0', 'id':0,
'method':'initialize',
'params':{
'protocolVersion':'2025-11-25',
'capabilities':{},
'clientInfo':{
'name':'ai-lab-client',
'version':'1.0.0'}}}
prepared = prepare_mcp_request(
new_mcp_client(), request)
print(prepared['client']['phase'])
# initializing
Een voorbereide request staat nog niet op de verbinding. De sessie zet hem straks werkelijk in de wachtrij; mislukt dat, dan wordt het openstaande verzoek ook afgehandeld. Een gebruikt ID blijft binnen die sessie gebruikt, ook na een fout of annulering.
012. Laat de server zijn eigen fase bewaken
Schrijf route_mcp_server voor de gegeven servertoestand. Initialize brengt de server naar initializing; pas notifications/initialized maakt hem ready. Ping mag binnen deze oefenvorm ook tijdens de start, zodat een controlesignaal geen tooluitvoering hoeft te zijn.
step = route_mcp_server(
new_mcp_server(), request)
print(step['state']['phase'])
print(len(step['outgoing']))
# initializing
# 1
Een toolrequest vóór ready krijgt de eigen oefencode -32002. Een notification levert geen response op, en een herhaald request-ID voert niets opnieuw uit. De server geeft de hostboolean werkelijk aan de dispatcher door: tools: beschrijft ondersteuning, terwijl de host een tool nog kan weigeren.
023. Controleer na het koppelen ook de inhoud
Schrijf accept_mcp_client_packet voor één transportpakket. De gegeven correlatiehelper kijkt eerst naar richting, generatie en ID. Daarna controleert jouw client de passende initresponse, toollijst, tooluitkomst of pinguitkomst.
link = new_mcp_link()
queue_mcp_message(link,
'server-to-client',
step['outgoing'][0])
packet = receive_mcp_packet(
link, 'server-to-client')
received = accept_mcp_client_packet(
prepared['client'], packet)
print(received['client']['phase'])
print(received['outgoing'][0]['method'])
# ready
# notifications/initialized
Deze drie kleine proeven controleren de losse stappen; de volgende cel verbindt het hele gesprek. Een nieuw gekoppeld antwoord heeft accepted True, ook als het daarna een ongeldig toolresultaat blijkt. Schema en tekstkopie moeten overeenkomen; een geldige berichtvorm bewijst op zichzelf geen juiste domeinuitkomst.
034. Verbind de echte wachtrijen
Schrijf run_mcp_session voor de begrensde gebeurtenislijst. Send bereidt voor en queue’t, server verwerkt één pakket en client ontvangt één pakket; initialisatiemeldingen gaan daadwerkelijk terug over de verbinding. Iedere uitgevoerde gebeurtenis krijgt een tracerij met openstaande verzoeken en cumulatieve pakketten en bytes.
Elke gebeurtenis heeft kind en precies de bijbehorende extra velden. De gegeven helpers controleren de request- en packetvorm; de sessie bewaakt de volgorde en sluiting.
| kind | Extra velden |
|---|---|
send | request |
server | geen |
client | geen |
cancel | id |
raw | direction, payload als bytes |
previous | packet uit een andere generatie |
timeout | geen |
close | geen |
cases = mcp_project_cases()
session = run_mcp_session(
cases[0]['events'])
print(session['link']['sent_messages'])
print(session['link']['sent_bytes'])
print(session['client']['registry'][
'settled'][-1]['message']['result'][
'structuredContent']['value'])
# 7
# 1319
# 6
Met de hand zijn er vier clientpakketten S: initialize, initialized, list en call. De server geeft drie antwoorden R; samen vormen ze N pakketten. De gegeven link noemt zijn teller sent_messages, maar bij kapotte invoer telt die ook onleesbare bytepakketten.
Een kapot pakket, timeout of uitgeput stappenbudget sluit de verbinding en handelt overige pending requests eenmaal af. De eerste sluitreden blijft bewaard. Een timeout is hier een expliciete, deterministische gebeurtenis; je meet daarmee geen wandkloktijd of productienetwerk.
cancel = run_mcp_session(
cases[7]['events'])
print(cancel['client']['registry'][
'settled'][-1]['status'])
print(cancel['link']['sent_messages'])
# cancelled
# 8
Hier was het toolantwoord al klaargezet toen de client annuleerde. Het late antwoord wordt genegeerd; de berekening wordt niet teruggedraaid. Pakketten uit een vorige generatie krijgen een aparte teller, zodat ze de bytes van deze verbinding niet vervalsen.
045. Laat de meting uit de uitvoering komen
Schrijf evaluate_mcp_sessions en plot_mcp_sessions. De twaalf gegeven proeven veranderen invoer, hostbeschikbaarheid of de ontvangen pakketten. Neem alleen gevalideerde multiply-inhoud als waarde over; afgewezen inhoud blijft None, met behoud van correlatiestatus en sluitreden.
report = evaluate_mcp_sessions(cases)
figure = plot_mcp_sessions(report)
print(len(report['rows']))
print(report['total_packets'])
print(report['total_bytes'])
# 12
# 74
# 11953
De twee grafiekpanelen tonen werkelijke pakketten en UTF-8-bytes per proef. Een extra ongeldig pakket kan meer verkeer opleveren en toch minder bruikbare uitkomsten geven. Lees daarom de grafiek samen met status, pending en reden; een lage balk is geen kwaliteitsbewijs.
05Wat neem je mee?
- Voorbereiden, versturen, koppelen en inhoudelijk controleren zijn afzonderlijke stappen.
- Eén sluiting ruimt beide rollen, de verbinding en alle wachtende verzoeken op.
- Een trace en grafiek horen bij werkelijk uitgevoerde gebeurtenissen.
Hierna onderzoek je hoe AI ook beelden, geluid en andere representaties verwerkt. Dezelfde gewoonte blijft helpen: maak grenzen zichtbaar en controleer de uitkomst tegen de taak.
Bronnen: MCP 2025-11-25 — levenscyclus en versieonderhandeling ↗ MCP 2025-11-25 — tools, discovery en resultaatvormen ↗ MCP 2025-11-25 — transport en berichtafhandeling ↗ Matplotlib — horizontale balken met Axes.barh ↗ · Gecontroleerd 2026-10-29.
1. Reserveer een request
De functies voor JSON, registers en clienttoestand zijn gegeven. Vul prepare_mcp_request bovenaan in; de helpers daaronder blijven zichtbaar zodat je hun grenzen kunt opzoeken.
Een request krijgt een plaats in pending voordat de host hem verstuurt. Controleer eerst fase en ondersteuning, zodat een mislukte voorbereiding niets aan de invoer verandert.
De kleine proef onderaan maakt initialize met ID 0. Verwacht initializing en één uitgaande request; er is in deze cel nog geen transportverzending.
Bekijk voorbeeld
def prepare_mcp_request(client,message):
client=validate_mcp_client(client);message=mcp_envelope(message)
if client['phase']=='closed' or mcp_message_kind(message)!='request':
raise ValueError('Request in een open client vereist')
method=message['method']
if method=='initialize':
if client['phase']!='new':raise ValueError('Initialize alleen vanuit new')
message=validate_mcp_initialize(message)
if message['params']['protocolVersion'] not in client['supported_versions']:
raise ValueError('Aangeboden versie zelf ondersteunen')
elif method!='ping' and client['phase']!='ready':
raise ValueError('Wacht op de voltooide initialisatie')
if method in ['tools/list','tools/call'] and 'tools' not in client['capabilities']:
raise ValueError('Server heeft geen toolondersteuning aangeboden')
client['registry']=track_mcp_request(client['registry'],message)
if method=='initialize':client.update(phase='initializing',initialize_request=copy.deepcopy(message))
return {'client':client,'outgoing':[message],'disposition':'request-prepared'}
# Gegeven helpers
import copy
import json
import math
import re
from decimal import Decimal, InvalidOperation
MCP_VERSION = '2025-11-25'
MCP_MAX_BYTES = 8192
MCP_INPUT_SCHEMA = {
'type':'object',
'properties':{'a':{'type':'integer','minimum':0,'maximum':1000},
'b':{'type':'integer','minimum':0,'maximum':1000}},
'required':['a','b'], 'additionalProperties':False}
MCP_OUTPUT_SCHEMA = {
'type':'object',
'properties':{'value':{'type':'integer','minimum':0,'maximum':1000000},
'status':{'type':'string','enum':['ok','invalid-arguments','limit-exceeded']}},
'required':['value','status'], 'additionalProperties':False}
def _mcp_integer(value, low, high):
if type(value) not in (int,float) or not low <= value <= high or not math.isfinite(value) or value != int(value):
raise ValueError('Een begrensde JSON-integerwaarde vereist; bool is geen getal')
return int(value)
def mcp_id_key(identifier):
"""Numeric 1 and 1.0 correlate; string '1' is a different ID."""
if type(identifier) is str:
if not re.fullmatch(r'[A-Za-z0-9_-]{1,40}',identifier):
raise ValueError('ID-tekst buiten de oefengrens')
return ('string',identifier)
return ('number',_mcp_integer(identifier,-1000000,1000000))
def _mcp_json_tree(value):
remaining = [512]
active = set()
def visit(item, depth):
remaining[0] -= 1
if remaining[0] < 0 or depth > 8:
raise ValueError('JSON-boom buiten de oefengrens')
if item is None or type(item) is bool:
return
if type(item) is str:
if len(item) > 4000:
raise ValueError('JSON-tekst te lang')
return
if type(item) in (int,float):
if abs(item) > 1000000000 or not math.isfinite(item):
raise ValueError('JSON-getal buiten de oefengrens')
return
if type(item) not in (dict,list) or id(item) in active:
raise ValueError('Alleen eindige JSON-bomen')
active.add(id(item))
if type(item) is dict:
if len(item) > 32 or any(type(key) is not str or len(key) > 128 for key in item):
raise ValueError('JSON-object buiten de oefengrens')
for key, child in item.items():
visit(key,depth+1); visit(child,depth+1)
else:
if len(item) > 64:
raise ValueError('JSON-lijst te lang')
for child in item:
visit(child,depth+1)
active.remove(id(item))
visit(value,0)
def _mcp_json_bytes(value):
_mcp_json_tree(value)
try:
payload = json.dumps(value,ensure_ascii=False,allow_nan=False,separators=(',',':')).encode('utf-8')
except (ValueError,UnicodeError) as error:
raise ValueError('Geen geldige UTF-8/JSON') from error
if len(payload) > MCP_MAX_BYTES:
raise ValueError('Pakket groter dan de oefengrens van8192bytes')
return payload
def _mcp_parse_json(text):
def pairs(items):
result = {}
for key,value in items:
if key in result:
raise ValueError('Dubbele JSON-objectsleutel')
result[key] = value
return result
def constant(value):
raise ValueError('Geen NaN of Infinity in JSON')
def decimal_number(token):
# Reject a lossy wire conversion rather than accepting a fractional ID
# or schema value as an integer after binary-float rounding/underflow.
try:
exact = Decimal(token)
number = float(token)
if not math.isfinite(number) or Decimal(str(number)) != exact:
raise ValueError('Wiregetal verliest decimale precisie in de oefenruntime')
return number
except (InvalidOperation,OverflowError,ValueError) as error:
raise ValueError('Wiregetal buiten de precieze oefenvorm') from error
try:
return json.loads(text,object_pairs_hook=pairs,parse_constant=constant,parse_float=decimal_number)
except (ValueError,RecursionError) as error:
raise ValueError('Ongeldige JSON') from error
def mcp_envelope(message):
"""Validate and copy the selected envelope shapes; no method authority."""
clean = _mcp_parse_json(_mcp_json_bytes(message).decode('utf-8'))
if type(clean) is not dict or clean.get('jsonrpc') != '2.0':
raise ValueError('Eén JSON-RPC2.0-object vereist')
fields = set(clean)
if 'method' in clean:
if not {'jsonrpc','method'} <= fields <= {'jsonrpc','method','id','params'}:
raise ValueError('Exact request-/notification-envelope')
if type(clean['method']) is not str or not re.fullmatch(r'[A-Za-z][A-Za-z0-9_/.-]{0,79}',clean['method']):
raise ValueError('Methode buiten de oefengrens')
if 'params' in clean and type(clean['params']) is not dict:
raise ValueError('MCP-params is een object')
if '_meta' in clean.get('params',{}):
validate_mcp_meta(clean['params']['_meta'],request=True)
if 'id' in clean:
mcp_id_key(clean['id'])
elif 'result' in clean:
if fields != {'jsonrpc','id','result'} or type(clean['result']) is not dict:
raise ValueError('Result-response heeft ID en objectresultaat')
mcp_id_key(clean['id'])
if '_meta' in clean['result']:
validate_mcp_meta(clean['result']['_meta'])
elif 'error' in clean:
if not {'jsonrpc','error'} <= fields <= {'jsonrpc','id','error'}:
raise ValueError('Exact error-envelope vereist')
if 'id' in clean:
mcp_id_key(clean['id'])
error = clean['error']
if type(error) is not dict or not {'code','message'} <= set(error) <= {'code','message','data'}:
raise ValueError('Error heeft code/message en eventueel data')
_mcp_integer(error['code'],-1000000,1000000)
if type(error['message']) is not str or not error['message'].strip() or len(error['message']) > 2000:
raise ValueError('Begrensde foutmelding vereist')
else:
raise ValueError('Request, notification, result of error vereist')
return clean
def mcp_message_kind(message):
clean = mcp_envelope(message)
if 'method' in clean:
return 'request' if 'id' in clean else 'notification'
return 'result' if 'result' in clean else 'error'
def encode_mcp_packet(message):
return _mcp_json_bytes(mcp_envelope(message))
def decode_mcp_packet(payload):
if type(payload) is not bytes or not 1 <= len(payload) <= MCP_MAX_BYTES:
raise ValueError('Eén begrensd bytespakket vereist')
try:
text = payload.decode('utf-8')
except UnicodeError as error:
raise ValueError('Pakket is geen UTF-8') from error
return mcp_envelope(_mcp_parse_json(text))
def validate_fixed_mcp_schema(schema,value):
"""Only these two exact classroom schemas, never arbitrary JSON Schema."""
try:
signature = json.dumps(schema,sort_keys=True,allow_nan=False,separators=(',',':'))
except (TypeError,ValueError,RecursionError) as error:
raise ValueError('Geen ondersteund oefenschema') from error
input_signature = json.dumps(MCP_INPUT_SCHEMA,sort_keys=True,separators=(',',':'))
output_signature = json.dumps(MCP_OUTPUT_SCHEMA,sort_keys=True,separators=(',',':'))
if type(value) is not dict:
raise ValueError('Schemas hebben een object als wortel')
if signature == input_signature:
if set(value) != {'a','b'}:
raise ValueError('Exact a en b vereist')
return {key:_mcp_integer(value[key],0,1000) for key in ['a','b']}
if signature == output_signature:
if set(value) != {'value','status'} or type(value['status']) is not str or value['status'] not in ['ok','invalid-arguments','limit-exceeded']:
raise ValueError('Exact value en bekende status vereist')
return {'value':_mcp_integer(value['value'],0,1000000),'status':value['status']}
raise ValueError('Onbekend dialect/keyword/schema: niet gevalideerd')
def validate_mcp_peer(info):
if type(info) is not dict or not {'name','version'} <= set(info):
raise ValueError('Peer heeft name en version')
if any(type(info[key]) is not str or not info[key].strip() or len(info[key]) > 80 for key in ['name','version']):
raise ValueError('Begrensde peeridentificatie vereist')
info = _mcp_parse_json(_mcp_json_bytes(info).decode('utf-8'))
for key in ['title','description','websiteUrl']:
if key in info and (type(info[key]) is not str or len(info[key]) > 2000):
raise ValueError('Optionele peerbeschrijving is begrensde tekst')
if 'icons' in info:
if type(info['icons']) is not list or len(info['icons']) > 16:
raise ValueError('Begrensde iconmetadata vereist; er wordt niets opgehaald')
for icon in info['icons']:
if type(icon) is not dict or 'src' not in icon or type(icon['src']) is not str:
raise ValueError('Iconmetadata heeft een tekstbron')
if 'mimeType' in icon and type(icon['mimeType']) is not str:
raise ValueError('MIME-metadata is tekst')
if 'sizes' in icon and (type(icon['sizes']) is not list or any(type(size) is not str for size in icon['sizes'])):
raise ValueError('Icongroottes zijn tekstwaarden')
if 'theme' in icon and icon['theme'] not in ['light','dark']:
raise ValueError('Bekend iconthema vereist')
return info
def validate_mcp_capabilities(capabilities,role='server'):
if type(capabilities) is not dict or any(type(value) is not dict for value in capabilities.values()):
raise ValueError('Capabilitynamen verwijzen naar objecten, ook een leeg object')
capabilities = _mcp_parse_json(_mcp_json_bytes(capabilities).decode('utf-8'))
client = {'experimental':'object-map','roots':{'listChanged':'bool'},
'sampling':{'context':'object','tools':'object'},'elicitation':{'form':'object','url':'object'},
'tasks':{'list':'object','cancel':'object','requests':{
'sampling':{'createMessage':'object'},'elicitation':{'create':'object'}}}}
server = {'experimental':'object-map','logging':'object','completions':'object',
'prompts':{'listChanged':'bool'},'resources':{'subscribe':'bool','listChanged':'bool'},
'tools':{'listChanged':'bool'},
'tasks':{'list':'object','cancel':'object','requests':{'tools':{'call':'object'}}}}
if role not in ['client','server']:
raise ValueError('Client- of servercapabilities vereist')
def check(data,shape):
for key,kind in shape.items():
if key not in data:
continue
value = data[key]
if kind == 'bool':
if type(value) is not bool:
raise ValueError('Bekende capabilityvlag is een boolean')
elif type(value) is not dict:
raise ValueError('Bekende capabilitystructuur is een object')
elif kind == 'object-map':
if any(type(item) is not dict for item in value.values()):
raise ValueError('Experimentele capabilitywaarden zijn objecten')
elif type(kind) is dict:
check(value,kind)
check(capabilities,client if role == 'client' else server)
# Unknown extensions remain bounded inert data; no automatic use/authority.
return capabilities
def validate_mcp_meta(metadata,request=False):
if type(metadata) is not dict:
raise ValueError('Metadata is een object')
metadata = _mcp_parse_json(_mcp_json_bytes(metadata).decode('utf-8'))
if request and 'progressToken' in metadata and type(metadata['progressToken']) not in (str,int,float):
raise ValueError('Een voortgangstoken is tekst of een getal, geen boolean')
return metadata
def validate_mcp_versions(versions):
if type(versions) not in (list,tuple) or not 1 <= len(versions) <= 4:
raise ValueError('1–4 eigen ondersteunde versies vereist')
if any(type(value) is not str or not value.strip() or len(value) > 40 for value in versions) or len(set(versions)) != len(versions):
raise ValueError('Unieke begrensde versieteksten vereist')
return list(versions)
def validate_mcp_initialize(request):
request = mcp_envelope(request)
if mcp_message_kind(request) != 'request' or request['method'] != 'initialize':
raise ValueError('Initialize-request vereist')
params = request.get('params')
required = {'protocolVersion','capabilities','clientInfo'}
if type(params) is not dict or not required <= set(params) <= required | {'_meta'}:
raise ValueError('Gekozen initialize-params vereist')
validate_mcp_versions([params['protocolVersion']])
validate_mcp_capabilities(params['capabilities'],role='client')
validate_mcp_peer(params['clientInfo'])
if '_meta' in params:
validate_mcp_meta(params['_meta'],request=True)
return request
def mcp_tool_definition():
return {'name':'multiply','description':'Vermenigvuldig twee eigen oefengetallen; productlimiet5000.',
'inputSchema':copy.deepcopy(MCP_INPUT_SCHEMA),'outputSchema':copy.deepcopy(MCP_OUTPUT_SCHEMA),
'annotations':{'readOnlyHint':True,'destructiveHint':False}}
def mcp_tool_result(value,status='ok'):
structured = validate_fixed_mcp_schema(MCP_OUTPUT_SCHEMA,{'value':value,'status':status})
if status != 'ok' and structured['value'] != 0:
raise ValueError('Eigen foutresultaat gebruikt waarde0')
return {'content':[{'type':'text','text':json.dumps(structured,separators=(',',':'))}],
'structuredContent':structured,'isError':status != 'ok'}
def validate_mcp_tool_result(result):
"""Selected text+structured result, including the tool's error branch."""
if type(result) is not dict or not {'content','structuredContent'} <= set(result) <= {'content','structuredContent','isError'}:
raise ValueError('Exact ondersteund toolresultaat vereist')
structured = validate_fixed_mcp_schema(MCP_OUTPUT_SCHEMA,result['structuredContent'])
is_error = result.get('isError',False)
if type(is_error) is not bool or is_error != (structured['status'] != 'ok'):
raise ValueError('isError en eigen status spreken elkaar tegen')
if is_error and structured['value'] != 0:
raise ValueError('Eigen foutresultaat gebruikt waarde0')
content = result['content']
if type(content) is not list or len(content) != 1 or type(content[0]) is not dict or set(content[0]) != {'type','text'}:
raise ValueError('Eén gegeven tekstkopie vereist')
if content[0]['type'] != 'text' or type(content[0]['text']) is not str or len(content[0]['text']) > 4000:
raise ValueError('Begrensde tekstkopie vereist')
mirrored = validate_fixed_mcp_schema(MCP_OUTPUT_SCHEMA,_mcp_parse_json(content[0]['text']))
if mirrored != structured:
raise ValueError('Tekst en gestructureerde data verschillen')
return copy.deepcopy({**result,'isError':is_error,'structuredContent':structured})
def client_initialize(request,response,supported_versions=(MCP_VERSION,)):
request = validate_mcp_initialize(request)
supported = validate_mcp_versions(supported_versions)
if request['params']['protocolVersion'] not in supported:
raise ValueError('Client moet de aangeboden versie zelf ondersteunen')
def outcome(phase,reason=None,version=None,capabilities=None,outgoing=None):
return {'phase':phase,'reason':reason,'version':version,
'capabilities':{} if capabilities is None else copy.deepcopy(capabilities),
'outgoing':[] if outgoing is None else copy.deepcopy(outgoing)}
try:
response = mcp_envelope(response)
except ValueError:
return outcome('closed','invalid-response')
kind = mcp_message_kind(response)
if kind not in ['result','error']:
return outcome('initializing','ignored-non-response')
if 'id' not in response:
return outcome('closed','uncorrelated-error')
if mcp_id_key(response['id']) != mcp_id_key(request['id']):
return outcome('initializing','unmatched-response')
if kind == 'error':
return outcome('closed','initialize-error')
result = response['result']
try:
if not {'protocolVersion','capabilities','serverInfo'} <= set(result):
raise ValueError('Initialize-resultaat mist velden')
validate_mcp_versions([result['protocolVersion']])
capabilities = validate_mcp_capabilities(result['capabilities'])
validate_mcp_peer(result['serverInfo'])
if 'instructions' in result and (type(result['instructions']) is not str or len(result['instructions']) > 2000):
raise ValueError('Optionele instructietekst is begrensde data')
except ValueError:
return outcome('closed','invalid-initialize-result')
if result['protocolVersion'] not in supported:
return outcome('closed','version-mismatch')
notification = {'jsonrpc':'2.0','method':'notifications/initialized'}
return outcome('ready',version=result['protocolVersion'],capabilities=capabilities,outgoing=[notification])
def _mcp_epoch(epoch):
if type(epoch) is not int or not 1 <= epoch <= 1000000:
raise ValueError('Eigen sessiegeneratie is een gewone int1–1000000')
return epoch
def _mcp_packet(packet):
if type(packet) is not dict or set(packet) != {'epoch','direction','payload'}:
raise ValueError('Exact transportpakket vereist')
_mcp_epoch(packet['epoch'])
if type(packet['direction']) is not str or packet['direction'] not in ['client-to-server','server-to-client']:
raise ValueError('Bekende transportrichting vereist')
if type(packet['payload']) is not bytes or len(packet['payload']) > MCP_MAX_BYTES:
raise ValueError('Begrensde bytespayload vereist')
return packet
def new_mcp_link(epoch=1):
return {'epoch':_mcp_epoch(epoch),'closed':False,'to_server':[],'to_client':[],
'sent_messages':0,'sent_bytes':0,'delivered_packets':0,'delivered_bytes':0,
'dropped_packets':0,'dropped_bytes':0}
def validate_mcp_link(link):
fields={'epoch','closed','to_server','to_client','sent_messages','sent_bytes',
'delivered_packets','delivered_bytes','dropped_packets','dropped_bytes'}
if type(link) is not dict or set(link) != fields or type(link['closed']) is not bool:
raise ValueError('Exacte interne transporttoestand vereist')
_mcp_epoch(link['epoch'])
for key in ['sent_messages','delivered_packets','dropped_packets']:
if type(link[key]) is not int or not 0 <= link[key] <= 128:
raise ValueError('Transportaantal buiten oefenlimiet')
for key in ['sent_bytes','delivered_bytes','dropped_bytes']:
if type(link[key]) is not int or not 0 <= link[key] <= 128*MCP_MAX_BYTES:
raise ValueError('Transportbytes buiten oefenlimiet')
queued=queued_bytes=0
for key,direction in [('to_server','client-to-server'),('to_client','server-to-client')]:
queue=link[key]
if type(queue) is not list or len(queue)>16:
raise ValueError('Maximaal16pakketten per wachtrij')
for packet in queue:
_mcp_packet(packet)
if packet['epoch'] != link['epoch'] or packet['direction'] != direction:
raise ValueError('Pakket hoort bij deze generatie en richting')
queued+=1;queued_bytes+=len(packet['payload'])
if link['sent_messages'] != link['delivered_packets']+link['dropped_packets']+queued:
raise ValueError('Transportaantallen sluiten niet aan')
if link['sent_bytes'] != link['delivered_bytes']+link['dropped_bytes']+queued_bytes:
raise ValueError('Transportbytes sluiten niet aan')
if link['closed'] and queued:
raise ValueError('Gesloten transport heeft lege wachtrijen')
return link
def queue_mcp_message(link,direction,message):
validate_mcp_link(link)
if type(direction) is not str or direction not in ['client-to-server','server-to-client']:
raise ValueError('Bekende richting vereist')
key='to_server' if direction=='client-to-server' else 'to_client'
if link['closed'] or len(link[key])==16 or link['sent_messages']==128:
raise ValueError('Transport gesloten of begrensde wachtrij vol')
payload=encode_mcp_packet(message)
packet={'epoch':link['epoch'],'direction':direction,'payload':payload}
link[key].append(packet)
link['sent_messages']+=1;link['sent_bytes']+=len(payload)
return copy.deepcopy(packet)
def receive_mcp_packet(link,direction):
validate_mcp_link(link)
if type(direction) is not str or direction not in ['client-to-server','server-to-client']:
raise ValueError('Bekende richting vereist')
key='to_server' if direction=='client-to-server' else 'to_client'
if not link[key]:
return None
packet=link[key].pop(0)
link['delivered_packets']+=1;link['delivered_bytes']+=len(packet['payload'])
return copy.deepcopy(packet)
def close_mcp_link(link):
validate_mcp_link(link)
for key in ['to_server','to_client']:
link['dropped_packets']+=len(link[key])
link['dropped_bytes']+=sum(len(packet['payload']) for packet in link[key])
link[key]=[]
link['closed']=True
return copy.deepcopy(link)
def new_mcp_registry(epoch=1,requester='client'):
if type(requester) is not str or requester not in ['client','server']:
raise ValueError('Bekende verzoeker vereist')
return {'epoch':_mcp_epoch(epoch),'requester':requester,'phase':'open',
'used':[],'pending':[],'settled':[],'reason':None}
def validate_mcp_registry(registry):
if type(registry) is not dict or set(registry) != {'epoch','requester','phase','used','pending','settled','reason'}:
raise ValueError('Exact eigen correlatieregister vereist')
_mcp_epoch(registry['epoch'])
if type(registry['requester']) is not str or registry['requester'] not in ['client','server'] or type(registry['phase']) is not str or registry['phase'] not in ['open','closed']:
raise ValueError('Bekende verzoeker en registerfase vereist')
for key,limit in [('used',64),('pending',16),('settled',64)]:
if type(registry[key]) is not list or len(registry[key])>limit:
raise ValueError('Registerlijst buiten de oefengrens')
used=[mcp_id_key(identifier) for identifier in registry['used']]
if len(set(used))!=len(used):
raise ValueError('Request-ID nooit hergebruiken binnen deze sessie')
active=[];settled=[]
for row in registry['pending']:
if type(row) is not dict or set(row)!={'id','method'} or type(row['method']) is not str or not re.fullmatch(r'[A-Za-z][A-Za-z0-9_/.-]{0,79}',row['method']):
raise ValueError('Pending record heeft ID en methode')
active.append(mcp_id_key(row['id']))
for row in registry['settled']:
if type(row) is not dict or set(row)!={'id','status','message','reason'} or type(row['status']) is not str or row['status'] not in ['result','error','cancelled','session-closed']:
raise ValueError('Bekende eigen afhandelingsrij vereist')
key=mcp_id_key(row['id']);settled.append(key)
if row['status'] in ['result','error']:
message=mcp_envelope(row['message'])
if mcp_message_kind(message)!=row['status'] or 'id' not in message or mcp_id_key(message['id'])!=key or row['reason'] is not None:
raise ValueError('Antwoord hoort bij de afgehandelde ID en status')
elif row['message'] is not None or type(row['reason']) is not str or not row['reason'].strip() or len(row['reason'])>200:
raise ValueError('Lokale afhandeling heeft een begrensde reden')
combined=active+settled
if len(set(combined))!=len(combined) or set(combined)!=set(used):
raise ValueError('Iedere gebruikte ID staat precies eenmaal pending of afgehandeld')
reason=registry['reason']
if registry['phase']=='open':
if reason is not None:raise ValueError('Open register heeft geen sluitreden')
elif registry['pending'] or type(reason) is not str or not reason.strip() or len(reason)>200:
raise ValueError('Gesloten register heeft geen pending en wel een reden')
return copy.deepcopy(registry)
def close_mcp_registry(registry,reason):
registry=validate_mcp_registry(registry)
if type(reason) is not str or not reason.strip() or len(reason)>200:
raise ValueError('Begrensde sluitreden vereist')
if registry['phase']=='closed':
return registry
for row in registry['pending']:
registry['settled'].append({'id':row['id'],'status':'session-closed','message':None,'reason':reason})
registry.update(phase='closed',reason=reason,pending=[])
return registry
def track_mcp_request(registry,message):
registry=validate_mcp_registry(registry)
message=mcp_envelope(message)
if registry['phase']!='open' or mcp_message_kind(message)!='request':
raise ValueError('Registreer alleen requests in een open sessie')
if 'task' in message.get('params',{}):
raise ValueError('Task-augmentation valt buiten dit oefenprofiel')
key=mcp_id_key(message['id'])
if key in [mcp_id_key(identifier) for identifier in registry['used']] or len(registry['used'])==64 or len(registry['pending'])==16:
raise ValueError('ID hergebruikt of begrensd register vol')
registry['used'].append(message['id'])
registry['pending'].append({'id':message['id'],'method':message['method']})
return registry
def receive_mcp_response(registry,packet):
registry=validate_mcp_registry(registry)
packet=_mcp_packet(packet)
def outcome(state,accepted,disposition):
return {'state':state,'accepted':accepted,'disposition':disposition}
if registry['phase']=='closed':
return outcome(registry,False,'ignored-closed')
expected='server-to-client' if registry['requester']=='client' else 'client-to-server'
if packet['direction']!=expected:
return outcome(registry,False,'ignored-direction')
if packet['epoch']!=registry['epoch']:
return outcome(registry,False,'ignored-epoch')
try:
message=decode_mcp_packet(packet['payload'])
except ValueError:
return outcome(close_mcp_registry(registry,'invalid-packet'),False,'closed-invalid-packet')
kind=mcp_message_kind(message)
if kind not in ['result','error']:
return outcome(registry,False,'ignored-non-response')
if 'id' not in message:
return outcome(close_mcp_registry(registry,'uncorrelated-error'),False,'closed-uncorrelated-error')
key=mcp_id_key(message['id'])
matches=[i for i,row in enumerate(registry['pending']) if mcp_id_key(row['id'])==key]
if not matches:
disposition='ignored-settled-id' if key in [mcp_id_key(identifier) for identifier in registry['used']] else 'ignored-unknown-id'
return outcome(registry,False,disposition)
row=registry['pending'].pop(matches[0])
registry['settled'].append({'id':row['id'],'status':kind,'message':message,'reason':None})
return outcome(registry,True,'settled')
def cancel_mcp_request(registry,request_id):
registry=validate_mcp_registry(registry)
key=mcp_id_key(request_id)
matches=[i for i,row in enumerate(registry['pending']) if mcp_id_key(row['id'])==key]
if registry['phase']=='closed' or not matches:
return {'state':registry,'notification':None,'disposition':'not-pending'}
row=registry['pending'][matches[0]]
if row['method']=='initialize':
raise ValueError('Initialize niet cancelen; gebruik de initialisatietimeout')
registry['pending'].pop(matches[0])
registry['settled'].append({'id':row['id'],'status':'cancelled','message':None,'reason':registry['requester']+'-cancelled'})
notification={'jsonrpc':'2.0','method':'notifications/cancelled',
'params':{'requestId':row['id'],'reason':'Eigen oefenverzoek gestopt'}}
return {'state':registry,'notification':notification,'disposition':'cancel-requested'}
def new_mcp_client(epoch=1,supported_versions=(MCP_VERSION,)):
return {'phase':'new','registry':new_mcp_registry(epoch),'initialize_request':None,
'supported_versions':validate_mcp_versions(supported_versions),
'version':None,'capabilities':{},'reason':None}
def validate_mcp_client(client):
fields={'phase','registry','initialize_request','supported_versions','version','capabilities','reason'}
if type(client) is not dict or set(client)!=fields or type(client['phase']) is not str or client['phase'] not in ['new','initializing','ready','closed']:
raise ValueError('Exacte eigen clienttoestand vereist')
registry=validate_mcp_registry(client['registry'])
versions=validate_mcp_versions(client['supported_versions'])
if registry['requester']!='client' or (registry['phase']=='closed')!=(client['phase']=='closed') or client['reason']!=registry['reason']:
raise ValueError('Clientfase en correlatieregister horen bij elkaar')
request=client['initialize_request']
if request is not None:
request=validate_mcp_initialize(request)
if request['params']['protocolVersion'] not in versions:
raise ValueError('Client ondersteunt zijn eigen aangeboden versie')
if client['phase']=='new' and request is not None or client['phase'] in ['initializing','ready'] and request is None:
raise ValueError('Initialize-request hoort bij de clientfase')
if client['phase']=='initializing':
if not any(row['method']=='initialize' and mcp_id_key(row['id'])==mcp_id_key(request['id']) for row in registry['pending']):
raise ValueError('Initialiserende client wacht op zijn initialize-ID')
if client['phase']=='ready':
validate_mcp_capabilities(client['capabilities'])
rows=[row for row in registry['settled'] if mcp_id_key(row['id'])==mcp_id_key(request['id']) and row['status']=='result']
expected=client_initialize(request,rows[0]['message'],versions) if rows else None
if expected is None or expected['phase']!='ready' or expected['version']!=client['version'] or expected['capabilities']!=client['capabilities']:
raise ValueError('Klaar volgt uit een werkelijk passende initialize-response')
elif client['version'] is not None or type(client['capabilities']) is not dict or client['capabilities']!={}:
raise ValueError('Zonder ready zijn versie en capabilities leeg')
return copy.deepcopy({**client,'registry':registry,'initialize_request':request,'supported_versions':versions})
def close_mcp_client(client,reason):
client=validate_mcp_client(client)
client['registry']=close_mcp_registry(client['registry'],reason)
client.update(phase='closed',reason=client['registry']['reason'],version=None,capabilities={})
return client
request = {'jsonrpc':'2.0', 'id':0,
'method':'initialize','params':{
'protocolVersion':MCP_VERSION,
'capabilities':{},'clientInfo':{
'name':'ai-lab-client',
'version':'1.0.0'}}}
client = new_mcp_client()
prepared = prepare_mcp_request(
client, request)
print(prepared['client']['phase']
if isinstance(prepared, dict)
else None)
2. Bewaak de serverfase
Vul route_mcp_server bovenaan in. De eigen server bewaart new, initializing of ready en de IDs die hij al ontvangen heeft.
Initialize en initialized zijn verschillende stappen. De clientmelding krijgt geen antwoord; ping kan in iedere open fase een klein resultaat krijgen.
De dispatcher en echte multiply-tool zijn gegeven. Geef de hostboolean door en bewaar de invoer; de losse initialize-proef hoort initializing terug te geven.
Bekijk voorbeeld
def route_mcp_server(server,message):
server=validate_mcp_server(server)
message=mcp_envelope(message)
def outcome(disposition,outgoing=None):
return {'state':server,'outgoing':[] if outgoing is None else [copy.deepcopy(outgoing)],'disposition':disposition}
def error(code,text,disposition):
return outcome(disposition,{'jsonrpc':'2.0','id':message['id'],'error':{'code':code,'message':text}})
if server['phase']=='closed':
return outcome('ignored-closed')
kind=mcp_message_kind(message)
if kind in ['result','error']:
return outcome('ignored-response')
method=message['method']
if kind=='notification':
params=message.get('params',{})
if method=='notifications/initialized' and set(params)<={'_meta'} and server['phase']=='initializing':
server['phase']='ready'
return outcome('initialized')
return outcome('ignored-notification')
key=mcp_id_key(message['id'])
if key in [mcp_id_key(identifier) for identifier in server['seen']]:
return error(-32600,'Request-ID already used','duplicate-id')
if len(server['seen'])==64:
raise ValueError('Begrensd serverregister vol')
server['seen'].append(message['id'])
if method=='ping':
if set(message.get('params',{}))-{'_meta'}:
return error(-32602,'Unsupported ping parameters','rejected-params')
return outcome('ping-replied',{'jsonrpc':'2.0','id':message['id'],'result':{}})
if method=='initialize':
if server['phase']!='new':
return error(-32002,'Initialize not allowed in this state','rejected-state')
try:
reply=server_initialize(message)
except ValueError:
return error(-32602,'Invalid initialize parameters','rejected-params')
server['phase']='initializing'
return outcome('initialize-replied',reply)
if server['phase']!='ready':
return error(-32002,'Server not initialized','rejected-state')
reply=dispatch_mcp_tool(message,server['tools_available'])
return outcome('tool-replied' if method in ['tools/list','tools/call'] else 'method-error',reply)
# Gegeven helpers
def server_initialize(request):
request = validate_mcp_initialize(request)
# This server supports one fixed edition. A different offer receives that edition.
return {'jsonrpc':'2.0','id':request['id'],'result':{
'protocolVersion':MCP_VERSION,'capabilities':{'tools':{}},
'serverInfo':{'name':'ai-lab-server','version':'1.0.0'}}}
def validate_mcp_call(params):
if type(params) is not dict:
raise ValueError('Tools/call-params is een object')
params = _mcp_parse_json(_mcp_json_bytes(params).decode('utf-8'))
if not {'name'} <= set(params) <= {'name','arguments','_meta'}:
raise ValueError('Name en optioneel arguments/metadata vereist')
if type(params['name']) is not str or not re.fullmatch(r'[A-Za-z0-9_.-]{1,128}',params['name']):
raise ValueError('Toolnaam buiten de oefengrens')
arguments = params.get('arguments',{})
if type(arguments) is not dict or ('_meta' in params and type(params['_meta']) is not dict):
raise ValueError('Arguments en metadata zijn objecten')
if '_meta' in params:
validate_mcp_meta(params['_meta'],request=True)
return {**params,'arguments':arguments}
def execute_mcp_multiply(arguments):
try:
clean = validate_fixed_mcp_schema(MCP_INPUT_SCHEMA,arguments)
except ValueError:
return mcp_tool_result(0,'invalid-arguments')
product = clean['a'] * clean['b']
if product > 5000:
return mcp_tool_result(0,'limit-exceeded')
return mcp_tool_result(product)
def dispatch_mcp_tool(message,tools_available=True):
if type(tools_available) is not bool:
raise ValueError('Toolbeschikbaarheid is een expliciete hostkeuze')
message = mcp_envelope(message)
kind = mcp_message_kind(message)
if kind == 'notification':
return None
if kind != 'request':
raise ValueError('Response hoort bij clientcorrelatie, niet bij tooldispatch')
def error(code,text):
return {'jsonrpc':'2.0','id':message['id'],'error':{'code':code,'message':text}}
def result(value):
return {'jsonrpc':'2.0','id':message['id'],'result':copy.deepcopy(value)}
if message['method'] not in ['tools/list','tools/call']:
return error(-32601,'Method not found')
if not tools_available:
return error(-32601,'Tools capability unavailable')
params = message.get('params',{})
if message['method'] == 'tools/list':
if set(params) - {'_meta'} or ('_meta' in params and type(params['_meta']) is not dict):
return error(-32602,'Unsupported list parameters')
return result({'tools':[mcp_tool_definition()]})
try:
call = validate_mcp_call(params)
except ValueError:
return error(-32602,'Invalid tool call parameters')
if call['name'] != 'multiply':
return error(-32602,'Unknown tool')
return result(execute_mcp_multiply(call['arguments']))
def new_mcp_server(tools_available=True):
if type(tools_available) is not bool:
raise ValueError('Hostbeschikbaarheid is een echte boolean')
return {'phase':'new','seen':[],'tools_available':tools_available,'reason':None}
def validate_mcp_server(server):
if type(server) is not dict or set(server)!={'phase','seen','tools_available','reason'}:
raise ValueError('Exacte eigen servertoestand vereist')
if type(server['phase']) is not str or server['phase'] not in ['new','initializing','ready','closed'] or type(server['tools_available']) is not bool:
raise ValueError('Bekende serverfase en hostboolean vereist')
if type(server['seen']) is not list or len(server['seen'])>64:
raise ValueError('Maximaal64ontvangen clientrequest-IDs')
keys=[mcp_id_key(identifier) for identifier in server['seen']]
if len(set(keys))!=len(keys):
raise ValueError('Ontvangen request-IDs zijn getypeerd uniek')
reason=server['reason']
if server['phase']=='closed':
if type(reason) is not str or not reason.strip() or len(reason)>200:
raise ValueError('Gesloten server heeft een begrensde reden')
elif reason is not None:
raise ValueError('Open server heeft geen sluitreden')
return copy.deepcopy(server)
server = new_mcp_server()
server_reply = route_mcp_server(
server, request)
print(server_reply['state']['phase']
if isinstance(server_reply, dict)
else None)
3. Controleer het ontvangen antwoord
Vul accept_mcp_client_packet bovenaan in. De gegeven correlatiehelper koppelt eerst generatie, richting en ID, waarna jij het juiste soort resultaat controleert.
De discoveryvalidator accepteert de beschreven kleine toolvorm; de toolresultaatvalidator controleert schema en tekstkopie. Een passende protocolerror houdt de sessie open, terwijl kapotte inhoud de overige pending requests eenmaal laat sluiten.
De proef zet een echte initresponse op een geheugenqueue en haalt hem weer op. Met de eerdere voorbereiding verwacht je ready en één initialized-notification, die de sessie later echt verstuurt.
Bekijk voorbeeld
def accept_mcp_client_packet(client,packet):
client=validate_mcp_client(client)
previous=client['registry']['pending']
delivery=receive_mcp_response(client['registry'],packet)
client['registry']=delivery['state'];method=None
def outcome(disposition,data=None,outgoing=None):
return {'client':client,'outgoing':[] if outgoing is None else copy.deepcopy(outgoing),
'disposition':disposition,'accepted':delivery['accepted'],'method':method,'data':copy.deepcopy(data)}
if client['registry']['phase']=='closed':
client=close_mcp_client({**client,'phase':'closed','reason':client['registry']['reason'],'version':None,'capabilities':{}},client['registry']['reason'])
return outcome(delivery['disposition'])
if not delivery['accepted']:return outcome(delivery['disposition'])
row=client['registry']['settled'][-1];message=row['message']
method=next(item['method'] for item in previous if mcp_id_key(item['id'])==mcp_id_key(row['id']))
if method=='initialize':
initialized=client_initialize(client['initialize_request'],message,client['supported_versions'])
if initialized['phase']=='closed':
client=close_mcp_client({**client,'phase':'closed','reason':initialized['reason'],
'registry':close_mcp_registry(client['registry'],initialized['reason']),'version':None,'capabilities':{}},initialized['reason'])
return outcome('initialization-closed')
client.update(phase='ready',version=initialized['version'],capabilities=initialized['capabilities'])
return outcome('initialized',outgoing=initialized['outgoing'])
if row['status']=='error':return outcome('protocol-error',message['error'])
try:
if method=='tools/list':data=validate_mcp_discovery(message['result'])
elif method=='tools/call':data=validate_mcp_tool_result(message['result'])
elif method=='ping':
data=message['result']
if set(data)-{'_meta'}:raise ValueError('Pingresultaat is leeg of metadata')
else:raise ValueError('Succes op onbekende methode buiten dit profiel')
except ValueError:
reason={'tools/list':'invalid-tool-list','tools/call':'invalid-tool-result','ping':'invalid-ping-result'}.get(method,'unsupported-result')
client=close_mcp_client(client,reason)
return outcome('closed-'+reason)
return outcome('tool-result' if method=='tools/call' else 'tool-list' if method=='tools/list' else 'ping-result',data)
# Gegeven helpers
def validate_mcp_discovery(result):
result=_mcp_parse_json(_mcp_json_bytes(result).decode('utf-8'))
if type(result) is not dict or not {'tools'}<=set(result)<={'tools','_meta'} or type(result['tools']) is not list or len(result['tools'])!=1:
raise ValueError('Precies één eigen multiply-tool vereist')
if '_meta' in result:validate_mcp_meta(result['_meta'])
tool=result['tools'][0]
if type(tool) is not dict or not {'name','inputSchema','outputSchema'}<=set(tool)<={'name','inputSchema','outputSchema','description','annotations'}:
raise ValueError('Alleen de gekozen toolmetadata')
signature=lambda schema:json.dumps(schema,sort_keys=True,separators=(',',':'))
if tool['name']!='multiply' or signature(tool['inputSchema'])!=signature(MCP_INPUT_SCHEMA) or signature(tool['outputSchema'])!=signature(MCP_OUTPUT_SCHEMA):
raise ValueError('Eigen toolnaam en twee vaste schema’s vereist')
if 'description' in tool and (type(tool['description']) is not str or len(tool['description'])>2000):
raise ValueError('Toolbeschrijving is begrensde tekst')
annotations=tool.get('annotations',{})
if type(annotations) is not dict or any(key in annotations and type(annotations[key]) is not bool for key in ['readOnlyHint','destructiveHint','idempotentHint','openWorldHint']):
raise ValueError('Bekende annotationhints zijn booleans, geen toestemming')
if 'title' in annotations and (type(annotations['title']) is not str or len(annotations['title'])>2000):
raise ValueError('Optionele annotationtitle is begrensde tekst')
return result
demo_link = new_mcp_link()
queue_mcp_message(demo_link,
'server-to-client',
server_initialize(request))
packet = receive_mcp_packet(
demo_link, 'server-to-client')
client_step = None
if isinstance(prepared, dict):
client_step = accept_mcp_client_packet(
prepared['client'], packet)
print(client_step['client']['phase']
if isinstance(client_step, dict)
else None)
4. Verbind beide rollen
Vul run_mcp_session bovenaan in. De packetadapter en twaalf proefscripts zijn gegeven; de tabel in de les beschrijft hun exacte eventvormen.
Maak voor iedere uitvoering verse rollen en een verse link. De eerste drie cellen waren losse controles; deze cel verwerkt de hele normale conversatie via beide queues.
Bij sluiten worden de verbinding, beide rollen en alle overige pending samen afgehandeld. De voorbeeldsessie eindigt ready met zeven pakketten, 1319 bytes en geen pending requests; timeouts zijn expliciete scriptgebeurtenissen.
Bekijk voorbeeld
def run_mcp_session(events,epoch=1,tools_available=True,supported_versions=(MCP_VERSION,),max_steps=32):
link=new_mcp_link(epoch);server=new_mcp_server(tools_available);client=new_mcp_client(epoch,supported_versions)
if type(events) is not list or not 1<=len(events)<=32 or type(max_steps) is not int or not 1<=max_steps<=32:
raise ValueError('1–32events en1–32stappen vereist')
shapes={'send':{'kind','request'},'server':{'kind'},'client':{'kind'},'cancel':{'kind','id'},
'raw':{'kind','direction','payload'},'previous':{'kind','packet'},'timeout':{'kind'},'close':{'kind'}}
for event in events:
if type(event) is not dict or type(event.get('kind')) is not str or event['kind'] not in shapes or set(event)!=shapes[event['kind']]:
raise ValueError('Exacte bekende eventvorm vereist')
kind=event['kind']
if kind=='send' and mcp_message_kind(event['request'])!='request':raise ValueError('Een eigen request vereist')
if kind=='cancel':mcp_id_key(event['id'])
if kind=='raw':_mcp_packet({'epoch':epoch,'direction':event['direction'],'payload':event['payload']})
if kind=='previous':
_mcp_packet(event['packet'])
if event['packet']['epoch']==epoch:raise ValueError('Previous komt uit een andere generatie')
events=copy.deepcopy(events);trace=[];previous=0
def close(reason):
nonlocal client
reason=client['reason'] or server['reason'] or reason
client=close_mcp_client(client,reason)
server.update(phase='closed',reason=reason)
close_mcp_link(link)
for event in events:
if client['phase']=='closed':break
if len(trace)==max_steps:
close('step-budget');break
kind=event['kind'];disposition='idle'
try:
if kind=='send':
prepared=prepare_mcp_request(client,event['request']);client=prepared['client']
for message in prepared['outgoing']:queue_mcp_message(link,'client-to-server',message)
disposition=prepared['disposition']
elif kind=='server':
served=serve_mcp_once(link,server);server=served['server'];disposition=served['disposition']
elif kind in ['client','previous']:
packet=event['packet'] if kind=='previous' else receive_mcp_packet(link,'server-to-client')
if kind=='previous':previous+=1
if packet is not None:
accepted=accept_mcp_client_packet(client,packet);client=accepted['client'];disposition=accepted['disposition']
for message in accepted['outgoing']:queue_mcp_message(link,'client-to-server',message)
elif kind=='cancel':
cancelled=cancel_mcp_request(client['registry'],event['id']);client['registry']=cancelled['state']
if cancelled['notification'] is not None:queue_mcp_message(link,'client-to-server',cancelled['notification'])
disposition=cancelled['disposition']
elif kind=='raw':
queue_mcp_payload(link,event['direction'],event['payload']);disposition='payload-queued'
elif kind=='timeout':
if not client['registry']['pending']:raise ValueError('Geen wachtend verzoek')
close('initialization-timeout' if client['phase']=='initializing' else 'request-timeout');disposition='closed'
else:close('transport-closed');disposition='closed'
if client['phase']=='closed' or server['phase']=='closed':close(client['reason'] or server['reason'])
except ValueError:
close('event-rejected');disposition='event-rejected'
trace.append({'event':kind,'disposition':disposition,'pending':len(client['registry']['pending']),
'packets':link['sent_messages'],'bytes':link['sent_bytes']})
if client['phase']!='closed' and client['registry']['pending']:
close('initialization-timeout' if client['phase']=='initializing' else 'request-timeout')
return {'link':link,'server':server,'client':client,'trace':trace,'previous_packets':previous}
# Gegeven helpers
def queue_mcp_payload(link,direction,payload):
"""Given bounded fault harness: send actual bytes, including bad MCP JSON."""
validate_mcp_link(link)
if type(direction) is not str or direction not in ['client-to-server','server-to-client']:
raise ValueError('Bekende transportrichting vereist')
if type(payload) is not bytes or len(payload)>MCP_MAX_BYTES:
raise ValueError('Begrensde bytespayload vereist')
key='to_server' if direction=='client-to-server' else 'to_client'
if link['closed'] or len(link[key])==16 or link['sent_messages']==128:
raise ValueError('Transport gesloten of begrensd vol')
packet={'epoch':link['epoch'],'direction':direction,'payload':payload}
link[key].append(packet)
link['sent_messages']+=1;link['sent_bytes']+=len(payload)
return copy.deepcopy(packet)
def serve_mcp_once(link,server):
"""Consume at most one actual packet; copy server state, mutate given link.
Invalid host link/server metadata is API misuse: ValueError before effects.
A valid host packet with invalid wire data closes the transport explicitly.
The future session driver must propagate that closure to the client registry.
"""
validate_mcp_link(link)
server=validate_mcp_server(server)
packet=None;decoded=0;outgoing=[]
def outcome(disposition,reason=None):
return {'server':server,'disposition':disposition,'reason':reason,
'incoming':copy.deepcopy(packet),'outgoing':copy.deepcopy(outgoing),
'received_packets':int(packet is not None),'received_messages':decoded,
'sent_packets':len(outgoing)}
def close(reason):
server.update(phase='closed',reason=reason)
close_mcp_link(link)
return outcome('closed',reason)
if server['phase']=='closed':
return close(server['reason'])
if link['closed']:
return close('transport-closed')
packet=receive_mcp_packet(link,'client-to-server')
if packet is None:
return outcome('idle')
try:
message=decode_mcp_packet(packet['payload'])
except ValueError:
return close('invalid-packet')
decoded=1
try:
routed=route_mcp_server(server,message)
except ValueError:
return close('server-request-rejected')
server=routed['state']
try:
for message in routed['outgoing']:
outgoing.append(queue_mcp_message(link,'server-to-client',message))
except ValueError:
return close('outgoing-rejected')
return outcome(routed['disposition'])
def mcp_project_cases():
initialize={'jsonrpc':'2.0','id':0,'method':'initialize','params':{
'protocolVersion':MCP_VERSION,'capabilities':{},'clientInfo':{'name':'ai-lab-client','version':'1.0.0'}}}
listing={'jsonrpc':'2.0','id':1,'method':'tools/list'}
def exchange(message):return [{'kind':'send','request':copy.deepcopy(message)},{'kind':'server'},{'kind':'client'}]
start=exchange(initialize)+[{'kind':'server'}]
def call(arguments):return {'jsonrpc':'2.0','id':2,'method':'tools/call','params':{'name':'multiply','arguments':arguments}}
def program(arguments):return start+exchange(listing)+exchange(call(arguments))
def case(identifier,events,**options):return {'id':identifier,'events':copy.deepcopy(events),'options':options}
normal=program({'a':2,'b':3})
offered=copy.deepcopy(initialize);offered['params']['protocolVersion']='offered'
old=new_mcp_link(1);queue_mcp_payload(old,'server-to-client',b'{')
previous=receive_mcp_packet(old,'server-to-client')
duplicate=encode_mcp_packet({'jsonrpc':'2.0','id':2,'result':mcp_tool_result(6)})
no_id=encode_mcp_packet({'jsonrpc':'2.0','error':{'code':-32700,'message':'ID onleesbaar'}})
waiting=start+[{'kind':'send','request':call({'a':2,'b':3})}]
return [case('goed',normal),case('tool-limiet',program({'a':100,'b':51})),
case('boolean-invoer',program({'a':True,'b':3})),case('protocolfout',program([])),
case('hostweigering',normal,tools_available=False),
case('versiebreuk',exchange(offered),supported_versions=['offered']),
case('kapot-pakket',waiting+[{'kind':'raw','direction':'server-to-client','payload':b'{'},{'kind':'client'}]),
case('annuleren',normal[:-1]+[{'kind':'cancel','id':2},{'kind':'client'},{'kind':'server'}]),
case('oud-pakket',start+[{'kind':'previous','packet':previous}]+normal[len(start):],epoch=2),
case('dubbel',normal+[{'kind':'raw','direction':'server-to-client','payload':duplicate},{'kind':'client'}]),
case('timeout',waiting+[{'kind':'timeout'}]),
case('fout-zonder-id',waiting+[{'kind':'raw','direction':'server-to-client','payload':no_id},{'kind':'client'}])]
cases = mcp_project_cases()
session = None
if all(isinstance(value, dict) for value
in [prepared, server_reply,
client_step]):
session = run_mcp_session(
cases[0]['events'])
print((session['link']['sent_messages'],
session['link']['sent_bytes'])
if isinstance(session, dict)
else None)
5. Meet en teken wat er gebeurde
Vul evaluate_mcp_sessions en plot_mcp_sessions bovenaan in. Voer de twaalf echte scripts uit en haal de waarden uit hun sessieresultaten; de case-ID is alleen een label.
Een gecorreleerde result-rij kan juist ongeldige inhoud bevatten. Toon een multiply-waarde pas na toolresultaatcontrole en koppeling aan de werkelijk voorbereide request, en bewaar anders None en de reden.
Teken twee horizontale staafpanelen met dezelfde proefvolgorde. De twaalf rijen leveren 24 gemeten balken; een korter mislukt gesprek is daarmee nog geen beter resultaat.
Bekijk voorbeeld
def evaluate_mcp_sessions(cases):
if type(cases) is not list or not 1<=len(cases)<=12:
raise ValueError('1–12eigen sessieproeven vereist')
rows=[];seen=set()
for case in cases:
if type(case) is not dict or set(case)!={'id','events','options'} or type(case['id']) is not str or not re.fullmatch(r'[a-z][a-z0-9-]{0,31}',case['id']) or case['id'] in seen:
raise ValueError('Unieke proef-ID en expliciete events/options vereist')
if type(case['options']) is not dict or set(case['options'])-{'epoch','tools_available','supported_versions','max_steps'}:
raise ValueError('Alleen bekende sessieopties')
seen.add(case['id']);session=run_mcp_session(case['events'],**case['options'])
client=session['client'];settled=client['registry']['settled'];last=settled[-1] if settled else None
sent=[event['request'] for event,step in zip(case['events'],session['trace']) if event['kind']=='send' and step['disposition']=='request-prepared']
called=next((message for message in sent if last and mcp_id_key(message['id'])==mcp_id_key(last['id'])),None)
data=None
if last and last['status']=='result' and called and called['method']=='tools/call' and called.get('params',{}).get('name')=='multiply':
try:data=validate_mcp_tool_result(last['message']['result'])['structuredContent']
except ValueError:pass
rows.append({'id':case['id'],'phase':client['phase'],'reason':client['reason'],
'packets':session['link']['sent_messages'],'bytes':session['link']['sent_bytes'],
'pending':len(client['registry']['pending']),'last_status':None if last is None else last['status'],
'value':None if data is None else data['value'],'tool_status':None if data is None else data['status'],
'previous_packets':session['previous_packets'],'trace_events':len(session['trace'])})
return {'rows':rows,'total_packets':sum(row['packets'] for row in rows),'total_bytes':sum(row['bytes'] for row in rows)}
def plot_mcp_sessions(report):
rows=report['rows'];labels=[row['id'] for row in rows]
figure,axes=plt.subplots(2,1,figsize=(4.2,8.8),layout='constrained')
for axis,key,title in zip(axes,['packets','bytes'],['Verzonden pakketten','Werkelijke UTF-8-bytes']):
values=[row[key] for row in rows]
axis.barh(labels,values,color='#236b4f');axis.invert_yaxis()
axis.set_title(title,fontsize=12);axis.tick_params(labelsize=11.5)
axis.spines[['top','right']].set_visible(False)
axis.grid(axis='x',alpha=.2);axis.set_axisbelow(True)
return figure
# Gegeven helpers
import matplotlib.pyplot as plt
report = None
figure = None
if isinstance(session, dict):
report = evaluate_mcp_sessions(cases)
if isinstance(report, dict):
figure = plot_mcp_sessions(report)
print((len(report['rows']),
report['total_packets'],
report['total_bytes'])
if isinstance(report, dict)
else None)