v2 - new interface, many new page for fine tuning
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||
Reference in New Issue
Block a user