Files
Alfredv2/alfred/app/mcp/alfred_developer_mcp.py
T

85 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)