import os, re, json, threading, requests
_URL = os.getenv("PROJECTS_MCP_URL", "http://localhost:8081")
_KEY = os.getenv("PROJECTS_MCP_API_KEY", "")
_rpc_id = 0; _id_lock = threading.Lock()
_session_id = None; _initialized = False; _init_lock = threading.Lock()
def _next_id():
global _rpc_id
with _id_lock: _rpc_id += 1; return _rpc_id
def _headers():
hdrs = {"Authorization": f"Bearer {_KEY}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"}
if _session_id: hdrs["Mcp-Session-Id"] = _session_id
return hdrs
def _parse_sse_body(resp, msg_id):
data_buf = []
for raw in resp.iter_lines(decode_unicode=True):
if raw is None: continue
if raw.startswith("data:"): data_buf.append(raw[5:].strip())
elif raw == "":
if not data_buf: continue
payload = json.loads("\n".join(data_buf)); data_buf = []
if isinstance(payload, dict) and payload.get("id") == msg_id:
return payload
raise RuntimeError("[MCP Proyectos] Stream SSE cerrado sin respuesta")
def _post_mcp(payload, timeout):
resp = requests.post(f"{_URL}/mcp", headers=_headers(),
json=payload, stream=True, timeout=timeout)
resp.raise_for_status()
sid = resp.headers.get("Mcp-Session-Id")
if "text/event-stream" in resp.headers.get("Content-Type", ""):
result = _parse_sse_body(resp, payload.get("id"))
elif resp.content: result = resp.json()
else: result = {}
return result, sid
def _ensure_session(timeout=20):
global _session_id, _initialized
with _init_lock:
if _initialized: return
init = {"jsonrpc": "2.0", "id": _next_id(), "method": "initialize",
"params": {"protocolVersion": "2025-03-26", "capabilities": {},
"clientInfo": {"name": "s1-crewai", "version": "1.0.0"}}}
result, sid = _post_mcp(init, timeout)
if sid: _session_id = sid
try: _post_mcp({"jsonrpc":"2.0","method":"notifications/initialized","params":{}}, timeout)
except: pass
_initialized = True
def _sse_call(method, params, timeout=20):
_ensure_session(timeout)
msg_id = _next_id()
result, sid = _post_mcp({"jsonrpc":"2.0","id":msg_id,"method":method,"params":params}, timeout)
if sid: global _session_id; _session_id = sid
if "error" in result: raise RuntimeError(f"[MCP Proyectos] Error RPC: {result['error']}")
return result.get("result", {})
def call_tool(tool, params):
result = _sse_call("tools/call", {"name": tool, "arguments": params})
content = result.get("content", [])
text = content[0]["text"] if content and content[0].get("type") == "text" else None
if result.get("isError"): raise RuntimeError(f"[MCP Proyectos] Tool '{tool}' falló: {text or result}")
if text is not None:
try: return json.loads(text)
except: return {"raw": text}
return result
_OWNER = os.getenv("GITHUB_OWNER", "")
_REPO = os.getenv("GITHUB_REPO", "")
def list_tools(): return _sse_call("tools/list", {}).get("tools", [])
def list_projects():
result = call_tool("manage_project", {"action":"list","status":"all","limit":20})
raw = result if isinstance(result, list) else result.get("projects", result.get("items", []))
return [{"id": p.get("id"), "title": p.get("title","(sin título)"),
"closed": bool(p.get("closed"))} for p in raw]
def list_project_items(project_id, limit=100):
for params in [{"action":"filter_items","projectId":project_id,"filter":{}},
{"action":"filter_items","projectId":project_id,"filterQuery":""},
{"action":"list_items", "projectId":project_id,"limit":limit}]:
try:
result = call_tool("manage_project", params)
raw = (result if isinstance(result, list)
else result.get("structuredContent",{}).get("items")
or result.get("items", []))
return [_normalize_project_item(it) for it in raw]
except RuntimeError: continue
raise RuntimeError("[MCP Proyectos] No se pudo listar items del proyecto")
def get_feature_from_item(item):
number = item.get("number")
if number:
r = call_tool("manage_issues", {"action":"get","owner":_OWNER,
"repo":_REPO,"issueNumber":number})
issue = r.get("issue", r) if isinstance(r, dict) else r
body = issue.get("body") or ""
return {"title": issue.get("title",""), "description": body,
"acceptance_criteria": _extract_acceptance_criteria(body)}
return {"title": item.get("title","(sin título)"),
"description": item.get("description",""),
"acceptance_criteria": item.get("acceptance_criteria", [])}