85 lines
4.8 KiB
Python
85 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Restricted Alfred Developer MCP (stdio, pure stdlib JSON-RPC).
|
||
|
||
This MCP intentionally exposes NO deploy, rollback, shell, filesystem deletion,
|
||
network fetch or arbitrary test execution operations.
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
PRODUCTION=os.environ.get('ALFRED_DEV_PRODUCTION','/opt/alfred/app')
|
||
sys.path.insert(0,PRODUCTION)
|
||
|
||
from services import alfred_developer_core as dev
|
||
|
||
|
||
def tool(name, description, props=None, required=None):
|
||
return {'name':name,'description':description,'inputSchema':{'type':'object','properties':props or {},'required':required or [],'additionalProperties':False}}
|
||
|
||
PID={'type':'string','pattern':'^[a-f0-9]{12}$','description':'ID renvoyé par create_task'}
|
||
REL={'type':'string','description':'Chemin relatif (templates/, static/js/, static/css/, services/ ou app.py), jamais .env/config/secrets.'}
|
||
TOOLS=[
|
||
tool('create_task','Créer une copie Git des sources autorisées dans /opt/alfred-dev. Aucun fichier de production modifié.',{'request_text':{'type':'string','minLength':5,'maxLength':2000}},['request_text']),
|
||
tool('list_source','Lister les fichiers de la copie de développement.',{'task_id':PID,'prefix':{'type':'string'}},['task_id']),
|
||
tool('read_source','Lire un fichier source de la copie.',{'task_id':PID,'path':REL},['task_id','path']),
|
||
tool('modify_source','Modifier ou créer UN fichier dans la copie Git ; aucun accès à la production.',{'task_id':PID,'path':REL,'content':{'type':'string','maxLength':180000}},['task_id','path','content']),
|
||
tool('run_checks','Effectuer seulement des contrôles statiques (AST Python, node --check, HTML, CSS). Ne lance jamais le code modifié.',{'task_id':PID},['task_id']),
|
||
tool('preview_changes','Afficher le diff et les contrôles préparés pour /maintenance.',{'task_id':PID},['task_id']),
|
||
tool('submit_proposal','Geler une proposition testée et l’envoyer dans /maintenance pour approbation humaine.',{'task_id':PID},['task_id']),
|
||
tool('list_tasks','Lister les propositions de développement. Pas de déploiement depuis le MCP.'),
|
||
]
|
||
|
||
|
||
def dispatch(name,args):
|
||
assert isinstance(args,dict)
|
||
if name=='create_task':return dev.create_task(args['request_text'])
|
||
if name=='list_source':return {'files':dev.list_source(args['task_id'],args.get('prefix',''))}
|
||
if name=='read_source':return dev.read_source(args['task_id'],args['path'])
|
||
if name=='modify_source':return dev.change_source(args['task_id'],args['path'],args['content'])
|
||
if name=='run_checks':return dev.run_checks(args['task_id'])
|
||
if name=='preview_changes':return dev.task_detail(args['task_id'])
|
||
if name=='submit_proposal':return dev.submit(args['task_id'])
|
||
if name=='list_tasks':return {'tasks':dev.list_tasks()}
|
||
raise ValueError('Outil inexistant.')
|
||
|
||
|
||
def send(obj):
|
||
sys.stdout.write(json.dumps(obj,ensure_ascii=False,separators=(',',':'))+'\n');sys.stdout.flush()
|
||
|
||
|
||
def handle(message):
|
||
if not isinstance(message,dict) or 'id' not in message:return
|
||
mid=message['id'];method=message.get('method')
|
||
if method=='initialize':
|
||
params=message.get('params') or {}
|
||
version=params.get('protocolVersion')
|
||
if version not in ('2025-06-18','2025-03-26','2024-11-05'):version='2025-06-18'
|
||
result={'protocolVersion':version,'capabilities':{'tools':{'listChanged':False}},
|
||
'serverInfo':{'name':'alfred-developer','version':'1.0.0'},
|
||
'instructions':'Modification UNIQUEMENT d’une copie Git. Après contrôles, soumettre à /maintenance. Jamais utiliser Terminal pour appliquer une modification en production.'}
|
||
elif method=='tools/list':result={'tools':TOOLS}
|
||
elif method=='ping':result={}
|
||
elif method=='tools/call':
|
||
try:
|
||
params=message.get('params') or {}
|
||
name=params.get('name');args=params.get('arguments') or {}
|
||
schema=next((t['inputSchema'] for t in TOOLS if t['name']==name),None)
|
||
if schema is None:raise ValueError('Outil inconnu.')
|
||
if not isinstance(args,dict) or set(args)-set(schema['properties']):raise ValueError('Arguments non autorisés.')
|
||
for key in schema['required']:
|
||
if key not in args:raise ValueError('Argument requis : '+key)
|
||
data=dispatch(name,args)
|
||
result={'content':[{'type':'text','text':json.dumps(data,ensure_ascii=False)}],'isError':False}
|
||
except Exception as exc:
|
||
result={'content':[{'type':'text','text':str(exc)[:900]}],'isError':True}
|
||
else:
|
||
send({'jsonrpc':'2.0','id':mid,'error':{'code':-32601,'message':'Méthode inconnue'}});return
|
||
send({'jsonrpc':'2.0','id':mid,'result':result})
|
||
|
||
if __name__=='__main__':
|
||
for line in sys.stdin:
|
||
if line.strip():
|
||
try:handle(json.loads(line))
|
||
except Exception as exc:print('MCP parsing :',str(exc)[:150],file=sys.stderr)
|