11f9833aac
- Schema worklog_comments (id, discussion, parent, datas, staff, campos parseados em JSON) - Parser HTML tolerante (h2/h3/h4) extrai title, task_ref, duration, work_items, files_modified, problems, patterns_text, actions - Módulo worklog-import com paginação MCP get_discussion_comments - Helper mcp-client.ts partilhado (gateway MCP JSON-RPC + SSE) - Dep runtime: node-html-parser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
/**
|
|
* Cliente HTTP mínimo para o gateway MCP (JSON-RPC 2.0 sobre HTTP).
|
|
*
|
|
* Suporta resposta em JSON puro ou SSE (text/event-stream). Partilhado entre
|
|
* os scripts de Observabilidade (patterns + worklog import).
|
|
*/
|
|
|
|
export interface MCPToolCallResult {
|
|
content?: Array<{ type: string; text: string }>
|
|
isError?: boolean
|
|
}
|
|
|
|
export async function callMcpTool(
|
|
tool: string,
|
|
args: Record<string, unknown>,
|
|
): Promise<MCPToolCallResult> {
|
|
const url = process.env.MCP_GATEWAY_URL ?? 'https://gateway.descomplicar.pt/v1/desk-crm/mcp'
|
|
const token = process.env.MCP_GATEWAY_TOKEN
|
|
if (!token) throw new Error('MCP_GATEWAY_TOKEN não definido')
|
|
const body = {
|
|
jsonrpc: '2.0',
|
|
id: Date.now(),
|
|
method: 'tools/call',
|
|
params: { name: tool, arguments: args },
|
|
}
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json, text/event-stream',
|
|
},
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (!res.ok) {
|
|
const txt = await res.text().catch(() => '')
|
|
throw new Error(`MCP gateway ${res.status}: ${txt.slice(0, 300)}`)
|
|
}
|
|
const raw = await res.text()
|
|
let payload: string | null = null
|
|
for (const line of raw.split(/\r?\n/)) {
|
|
const trimmed = line.trim()
|
|
if (!trimmed) continue
|
|
if (trimmed.startsWith('data: ')) {
|
|
payload = trimmed.slice(6)
|
|
break
|
|
}
|
|
if (trimmed.startsWith('{')) {
|
|
payload = trimmed
|
|
break
|
|
}
|
|
}
|
|
if (!payload) throw new Error(`MCP resposta sem payload JSON: ${raw.slice(0, 200)}`)
|
|
const parsed = JSON.parse(payload) as { error?: unknown; result?: MCPToolCallResult }
|
|
if (parsed.error) throw new Error(`MCP error: ${JSON.stringify(parsed.error)}`)
|
|
const result = parsed.result as MCPToolCallResult | undefined
|
|
if (result?.isError) {
|
|
const txt = result.content?.map((c) => c.text).join('\n') ?? ''
|
|
throw new Error(`MCP tool ${tool} devolveu isError: ${txt.slice(0, 300)}`)
|
|
}
|
|
return result ?? {}
|
|
}
|
|
|
|
/** Extrai o primeiro bloco de texto JSON-encoded do resultado MCP. */
|
|
export function extractMcpJsonPayload<T = unknown>(r: MCPToolCallResult): T {
|
|
const text = r.content?.find((c) => c.type === 'text')?.text
|
|
if (!text) throw new Error('MCP result sem content text')
|
|
return JSON.parse(text) as T
|
|
}
|