v2 - new interface, many new page for fine tuning

This commit is contained in:
pporcheret
2026-10-28 21:45:40 +01:00
parent a6169bcfdb
commit a17c3cb614
89 changed files with 8691 additions and 13 deletions
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""MCP stdio JSONL Alfred Listes — stdlib uniquement, aucun sleep/terminal."""
from __future__ import annotations
import json
import os
import sys
from urllib import request, error
BASE = os.getenv('ALFRED_LISTS_API','http://127.0.0.1:54321').rstrip('/')
KIND = {'type':'string','enum':['courses','taches'],'description':'courses = liste de courses ; taches = liste de tâches.'}
LABEL = {'type':'string','minLength':1,'maxLength':160,'description':'Texte exact à ajouter, ex. pain, peindre le mur.'}
ID = {'type':'integer','minimum':1,'description':'ID fourni par list_items ; préférable au nom pour les suppressions.'}
def tool(name,desc,properties=None,required=None):
return {'name':name,'description':desc,
'inputSchema':{'type':'object','properties':properties or {},'required':required or [],'additionalProperties':False}}
TOOLS = [
tool('add_item','Ajoute immédiatement un élément à la liste de courses ou de tâches Alfred. Exemples : « ajoute du pain sur la liste de courses » => kind=courses label=pain ; « ajoute peindre le mur à ma liste de tâches » => kind=taches label=peindre le mur. Ne jamais confirmer sans succès.',{'kind':KIND,'label':LABEL},['kind','label']),
tool('list_items','Affiche les éléments enregistrés et leurs statuts, dans une seule liste ou les deux.',{'kind':KIND}),
tool('complete_item','Coche un élément comme terminé/acheté. Donner son id ou son label exact et sa liste.',{'kind':KIND,'id':ID,'label':LABEL},['kind']),
tool('reopen_item','Décoche un élément terminé.',{'kind':KIND,'id':ID,'label':LABEL},['kind']),
tool('delete_item','Supprime un élément précis demandé par utilisateur.',{'kind':KIND,'id':ID,'label':LABEL},['kind']),
tool('clear_completed','Supprime UNIQUEMENT les éléments déjà cochés de la liste indiquée, sur demande explicite.',{'kind':KIND},['kind']),
]
def invoke(name,args):
if name not in {t['name'] for t in TOOLS}: raise ValueError('Outil inconnu.')
if not isinstance(args,dict): raise ValueError('Arguments invalides.')
allowed=set(next(t['inputSchema']['properties'] for t in TOOLS if t['name']==name))
if any(key not in allowed for key in args): raise ValueError('Argument inattendu.')
if name == 'list_items':
# The GET endpoint always returns both lists; restrict tool answer if requested.
req = request.Request(BASE+'/api/alfred/lists',headers={'Accept':'application/json'})
else:
body=json.dumps({'action':name,**args},ensure_ascii=False).encode('utf-8')
req=request.Request(BASE+'/api/alfred/lists/action',data=body,method='POST',
headers={'Content-Type':'application/json','X-Alfred-MCP-Local':'lists'})
try:
with request.urlopen(req, timeout=8) as resp: data=json.loads(resp.read(1_000_000))
except error.HTTPError as exc:
try: reason=json.loads(exc.read()).get('error',f'HTTP {exc.code}')
except Exception: reason=f'HTTP {exc.code}'
raise ValueError(reason) from exc
except (error.URLError,TimeoutError) as exc:
raise ValueError(f'API Alfred indisponible : {exc}') from exc
if not data.get('ok'): raise ValueError(data.get('error','Erreur API Alfred.'))
if name == 'list_items' and args.get('kind'):
kind = args['kind']
if kind not in ('courses','taches'): raise ValueError('Liste invalide.')
data['lists']={kind:data['lists'][kind]}
data['counts']={kind:data['counts'][kind]}
return data
def reply(obj):
sys.stdout.write(json.dumps(obj,ensure_ascii=False,separators=(',',':'))+'\n');sys.stdout.flush()
def handle(obj):
if not isinstance(obj,dict) or 'id' not in obj:return
mid=obj['id'];method=obj.get('method','')
try:
if method=='initialize':
asked=(obj.get('params') or {}).get('protocolVersion','2025-06-18')
version=asked if asked in ('2025-06-18','2025-03-26','2024-11-05') else '2025-06-18'
out={'protocolVersion':version,'capabilities':{'tools':{'listChanged':False}},
'serverInfo':{'name':'alfred-listes','version':'1.0.0'},
'instructions':'Pour courses/tâches, utilise ces outils. Ne simule pas la création et confirme uniquement un succès API.'}
elif method=='tools/list':out={'tools':TOOLS}
elif method=='ping':out={}
elif method=='tools/call':
p=obj.get('params') or {}
try:
result=invoke(str(p.get('name','')),p.get('arguments') or {})
out={'content':[{'type':'text','text':json.dumps(result,ensure_ascii=False)}],'isError':False}
except Exception as e:
out={'content':[{'type':'text','text':str(e)}],'isError':True}
else:
reply({'jsonrpc':'2.0','id':mid,'error':{'code':-32601,'message':'Method not found'}});return
reply({'jsonrpc':'2.0','id':mid,'result':out})
except Exception as e:
reply({'jsonrpc':'2.0','id':mid,'error':{'code':-32603,'message':str(e)}})
if __name__=='__main__':
for line in sys.stdin:
if not line.strip():continue
try:handle(json.loads(line))
except (ValueError,TypeError) as exc:print('Erreur message MCP :',exc,file=sys.stderr)