- corrigido encoding UTF-8 nos spawns de Python do painel CEP para exibir caracteres acentuados corretamente - corrigido copiar erro técnico: forçado pbcopy com encoding utf-8 em vez de navigator.clipboard que corrompia caracteres Resumo: - 2 arquivos alterados - 0 novos - 2 modificados - 0 removidos 2 files changed, 8 insertions(+), 12 deletions(-) Arquivos: - code/cep-plugin/main.js - code/engine/integracoes/premiere/cliente_mcp.py
138 lines
6.0 KiB
Python
138 lines
6.0 KiB
Python
"""Cliente MCP (Model Context Protocol) para o servidor local do Premiere.
|
|
|
|
Fala o protocolo JSON-RPC do MCP por stdio com o processo Node do bridge
|
|
(``code/dist/index.js``). É a única camada do sistema que conhece o
|
|
transporte (subprocesso, linhas JSON, ids de requisição); o resto do código
|
|
usa só o método :meth:`ClienteMCP.chamar` com nome de ferramenta e
|
|
argumentos.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
import json
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
from .erros_mcp import ErroDeConexaoMCP, ErroDeFerramentaMCP
|
|
|
|
|
|
class ClienteMCP(ABC):
|
|
"""Define a comunicação técnica com o servidor MCP."""
|
|
|
|
@abstractmethod
|
|
def conectar(self) -> None:
|
|
"""Inicia a conexão com o servidor MCP. Não faz nada se já estiver conectado."""
|
|
|
|
@abstractmethod
|
|
def desconectar(self) -> None:
|
|
"""Encerra a conexão com o servidor MCP, se houver uma ativa."""
|
|
|
|
@abstractmethod
|
|
def esta_conectado(self) -> bool:
|
|
"""Indica se há uma conexão ativa com o servidor MCP."""
|
|
|
|
@abstractmethod
|
|
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
"""Executa uma ferramenta do servidor MCP e devolve o resultado.
|
|
|
|
Levanta :class:`~.erros_mcp.ErroDeConexaoMCP` se não houver conexão
|
|
ativa e :class:`~.erros_mcp.ErroDeFerramentaMCP` se o servidor
|
|
responder com erro.
|
|
"""
|
|
|
|
|
|
class ClienteMCPPorStdio(ClienteMCP):
|
|
"""Cliente MCP para o servidor local do Premiere via stdio."""
|
|
|
|
def __init__(self, comando: list[str], tempo_limite: float = 30.0) -> None:
|
|
self.comando = comando
|
|
self.tempo_limite = tempo_limite
|
|
self._processo: subprocess.Popen[str] | None = None
|
|
self._proximo_id = 1
|
|
|
|
def conectar(self) -> None:
|
|
"""Sobe o processo do servidor MCP e envia o handshake ``initialize``.
|
|
|
|
Não faz nada se um processo já estiver em execução — não é possível
|
|
reconectar sem antes chamar :meth:`desconectar`.
|
|
"""
|
|
if self._processo is not None:
|
|
return
|
|
self._processo = subprocess.Popen(
|
|
self.comando,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
encoding="utf-8",
|
|
bufsize=1,
|
|
)
|
|
self._enviar("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "engine", "version": "0.1.0"}})
|
|
|
|
def desconectar(self) -> None:
|
|
"""Encerra o processo do servidor MCP, se houver um em execução."""
|
|
if self._processo is not None:
|
|
self._processo.terminate()
|
|
self._processo = None
|
|
|
|
def esta_conectado(self) -> bool:
|
|
"""``True`` enquanto o processo do servidor MCP estiver vivo."""
|
|
return self._processo is not None and self._processo.poll() is None
|
|
|
|
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
"""Executa ``nome_da_ferramenta`` no servidor MCP com ``argumentos``.
|
|
|
|
Levanta :class:`~.erros_mcp.ErroDeConexaoMCP` se não houver conexão
|
|
ativa e :class:`~.erros_mcp.ErroDeFerramentaMCP` se o servidor
|
|
responder com um erro para essa ferramenta.
|
|
"""
|
|
if not self.esta_conectado():
|
|
raise ErroDeConexaoMCP("Cliente MCP não está conectado.")
|
|
resposta = self._enviar("tools/call", {"name": nome_da_ferramenta, "arguments": argumentos or {}})
|
|
if resposta.get("error"):
|
|
raise ErroDeFerramentaMCP(f"Erro ao executar {nome_da_ferramenta!r} no MCP: {resposta['error']}")
|
|
resultado = resposta.get("result", {})
|
|
# Uma ferramenta que falha não devolve erro no nível do JSON-RPC: o
|
|
# servidor responde "com sucesso" e marca `isError` no resultado. Sem
|
|
# olhar esse campo, uma escrita recusada pelo Premiere passaria por
|
|
# bem-sucedida — foi assim que uma chamada a uma ferramenta inexistente
|
|
# ficou invisível até o efeito aparecer na timeline.
|
|
if resultado.get("isError"):
|
|
raise ErroDeFerramentaMCP(
|
|
f"A ferramenta {nome_da_ferramenta!r} falhou: {self._mensagem_de_erro(resultado)}"
|
|
)
|
|
# O servidor MCP (SDK 2.0+) envolve o dado da ferramenta em
|
|
# ``structuredContent.data``. Desembrulha para que o chamador receba
|
|
# o objeto de resultado direto (ex.: ``{"totalTracks": 2, ...}``).
|
|
sc = resultado.get("structuredContent")
|
|
if isinstance(sc, dict) and "data" in sc:
|
|
return sc["data"]
|
|
return resultado
|
|
|
|
@staticmethod
|
|
def _mensagem_de_erro(resultado: dict[str, Any]) -> str:
|
|
"""Extrai a mensagem de erro que o servidor MCP devolveu no resultado."""
|
|
conteudo_estruturado = resultado.get("structuredContent")
|
|
if isinstance(conteudo_estruturado, dict) and conteudo_estruturado.get("error"):
|
|
return str(conteudo_estruturado["error"])
|
|
for bloco in resultado.get("content", []):
|
|
if isinstance(bloco, dict) and bloco.get("type") == "text":
|
|
return str(bloco.get("text", ""))
|
|
return "sem detalhes"
|
|
|
|
def _enviar(self, metodo: str, parametros: dict[str, Any]) -> dict[str, Any]:
|
|
"""Envia uma requisição JSON-RPC pela stdin do processo e lê a resposta da stdout.
|
|
|
|
Levanta :class:`~.erros_mcp.ErroDeConexaoMCP` se o processo não
|
|
estiver disponível ou encerrar sem responder.
|
|
"""
|
|
if self._processo is None or self._processo.stdin is None or self._processo.stdout is None:
|
|
raise ErroDeConexaoMCP("Processo MCP indisponível.")
|
|
identificador = self._proximo_id
|
|
self._proximo_id += 1
|
|
self._processo.stdin.write(json.dumps({"jsonrpc": "2.0", "id": identificador, "method": metodo, "params": parametros}) + "\n")
|
|
self._processo.stdin.flush()
|
|
linha = self._processo.stdout.readline()
|
|
if not linha:
|
|
raise ErroDeConexaoMCP("O MCP encerrou sem retornar resposta.")
|
|
return json.loads(linha)
|