@linkegringo/mcp 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -108,6 +108,10 @@ declare class BridgeJobStore extends EventEmitter {
108
108
  getWatcherCount(): number;
109
109
  hasActiveWatcher(): boolean;
110
110
  cancelAllPending(reason?: string): number;
111
+ resetQueue(reason?: string): {
112
+ canceledCount: number;
113
+ clearedCount: number;
114
+ };
111
115
  clear(): void;
112
116
  }
113
117
  declare const bridgeJobStore: BridgeJobStore;
@@ -463,4 +467,38 @@ declare function handleSubmitJobResult(input: SubmitJobResultInput): Promise<{
463
467
  };
464
468
  }>;
465
469
 
466
- export { type AuditIssue, type AuditProfileInput, type BridgeJob, type BridgeServerOptions, type ConvertToXyzBulletInput, type GenerateHeadlineInput, type GetPendingJobInput, type InstallResult, type InstallerOptions, type JobStatus, type JobType, type McpTarget, type SimulateRecruiterSearchInput, type SparseExperience, type SubmitJobResultInput, type WatcherOptions, auditProfileInputSchema, bridgeJobStore, completeRemoteOrLocalJob, convertToXyzBulletInputSchema, createBridgeHttpServer, createLinkeGringoMcpServer, detectSparseExperiences, failRemoteOrLocalJob, formatGoogleXyzBullet, generateHeadlineInputSchema, getExperienceBulletCount, getMcpConfigsForSystem, getPendingJobInputSchema, getRemoteOrLocalPendingJob, handleAuditProfile, handleConvertToXyzBullet, handleGenerateHeadline, handleGetPendingJob, handleSimulateRecruiterSearch, handleSubmitJobResult, installMcpServerConfig, parseArgs, runInstaller, simulateRecruiterSearchInputSchema, startBridgeServer, startBridgeWatcher, stopBridgeServer, submitJobResultInputSchema };
470
+ declare const watchLinkeGringoInputSchema: z.ZodObject<{
471
+ timeoutSeconds: z.ZodOptional<z.ZodNumber>;
472
+ bridgeUrl: z.ZodOptional<z.ZodString>;
473
+ }, "strip", z.ZodTypeAny, {
474
+ timeoutSeconds?: number | undefined;
475
+ bridgeUrl?: string | undefined;
476
+ }, {
477
+ timeoutSeconds?: number | undefined;
478
+ bridgeUrl?: string | undefined;
479
+ }>;
480
+ type WatchLinkeGringoInput = z.infer<typeof watchLinkeGringoInputSchema>;
481
+ declare function waitForNextJob(bridgeUrl: string, timeoutMs: number): Promise<BridgeJob | null>;
482
+ declare function handleWatchLinkeGringo(input?: WatchLinkeGringoInput): Promise<{
483
+ content: {
484
+ type: "text";
485
+ text: string;
486
+ }[];
487
+ structuredData: {
488
+ jobFound: boolean;
489
+ job: BridgeJob<any, any>;
490
+ };
491
+ } | {
492
+ content: {
493
+ type: "text";
494
+ text: string;
495
+ }[];
496
+ structuredData: {
497
+ status: string;
498
+ timeoutSeconds: number;
499
+ pendingCount: number;
500
+ serverOnline: boolean;
501
+ };
502
+ }>;
503
+
504
+ export { type AuditIssue, type AuditProfileInput, type BridgeJob, type BridgeServerOptions, type ConvertToXyzBulletInput, type GenerateHeadlineInput, type GetPendingJobInput, type InstallResult, type InstallerOptions, type JobStatus, type JobType, type McpTarget, type SimulateRecruiterSearchInput, type SparseExperience, type SubmitJobResultInput, type WatchLinkeGringoInput, type WatcherOptions, auditProfileInputSchema, bridgeJobStore, completeRemoteOrLocalJob, convertToXyzBulletInputSchema, createBridgeHttpServer, createLinkeGringoMcpServer, detectSparseExperiences, failRemoteOrLocalJob, formatGoogleXyzBullet, generateHeadlineInputSchema, getExperienceBulletCount, getMcpConfigsForSystem, getPendingJobInputSchema, getRemoteOrLocalPendingJob, handleAuditProfile, handleConvertToXyzBullet, handleGenerateHeadline, handleGetPendingJob, handleSimulateRecruiterSearch, handleSubmitJobResult, handleWatchLinkeGringo, installMcpServerConfig, parseArgs, runInstaller, simulateRecruiterSearchInputSchema, startBridgeServer, startBridgeWatcher, stopBridgeServer, submitJobResultInputSchema, waitForNextJob, watchLinkeGringoInputSchema };
package/dist/index.js CHANGED
@@ -150,6 +150,14 @@ var BridgeJobStore = class extends EventEmitter {
150
150
  this.emit("queue:cleared", count);
151
151
  return count;
152
152
  }
153
+ resetQueue(reason = "Cancelado pelo usu\xE1rio via interface web") {
154
+ const canceledCount = this.cancelAllPending(reason);
155
+ const clearedCount = this.jobs.size;
156
+ this.jobs.clear();
157
+ this.pendingQueue = [];
158
+ this.emit("queue:reset", { canceledCount, clearedCount });
159
+ return { canceledCount, clearedCount };
160
+ }
153
161
  clear() {
154
162
  this.jobs.clear();
155
163
  this.pendingQueue = [];
@@ -1388,6 +1396,214 @@ O front-end em \`localhost:5173\` acabou de receber a resposta formatada e atual
1388
1396
  }
1389
1397
  }
1390
1398
 
1399
+ // src/tools/watch-linkegringo.ts
1400
+ import { z as z13 } from "zod";
1401
+ import http from "http";
1402
+ var watchLinkeGringoInputSchema = z13.object({
1403
+ timeoutSeconds: z13.number().optional().describe(
1404
+ "Tempo m\xE1ximo em segundos para aguardar a chegada de um novo job da interface web do LinkeGringo (padr\xE3o: 60s, m\xE1x: 300s). Se j\xE1 houver um job na fila, retorna imediatamente."
1405
+ ),
1406
+ bridgeUrl: z13.string().optional().describe("URL base do bridge HTTP local do LinkeGringo (padr\xE3o: http://127.0.0.1:5174)")
1407
+ });
1408
+ async function waitForNextJob(bridgeUrl, timeoutMs) {
1409
+ const localPending = bridgeJobStore.getPendingJob();
1410
+ if (localPending) {
1411
+ return localPending;
1412
+ }
1413
+ return new Promise((resolve) => {
1414
+ let resolved = false;
1415
+ let req = null;
1416
+ let timer = null;
1417
+ const cleanup = () => {
1418
+ if (timer) {
1419
+ clearTimeout(timer);
1420
+ timer = null;
1421
+ }
1422
+ if (req) {
1423
+ try {
1424
+ req.destroy();
1425
+ } catch {
1426
+ }
1427
+ req = null;
1428
+ }
1429
+ bridgeJobStore.off("job:created", onLocalJob);
1430
+ };
1431
+ const done = (job) => {
1432
+ if (resolved) return;
1433
+ resolved = true;
1434
+ cleanup();
1435
+ resolve(job);
1436
+ };
1437
+ const onLocalJob = (job) => {
1438
+ done(job);
1439
+ };
1440
+ bridgeJobStore.on("job:created", onLocalJob);
1441
+ timer = setTimeout(() => {
1442
+ done(null);
1443
+ }, timeoutMs);
1444
+ try {
1445
+ const parsed = new URL("/api/jobs/stream", bridgeUrl);
1446
+ req = http.get(
1447
+ {
1448
+ hostname: parsed.hostname,
1449
+ port: parsed.port || 5174,
1450
+ path: parsed.pathname,
1451
+ headers: {
1452
+ Accept: "text/event-stream",
1453
+ "Cache-Control": "no-cache",
1454
+ Connection: "keep-alive"
1455
+ }
1456
+ },
1457
+ (res) => {
1458
+ if (res.statusCode !== 200) {
1459
+ return;
1460
+ }
1461
+ let buffer = "";
1462
+ res.setEncoding("utf8");
1463
+ res.on("data", (chunk) => {
1464
+ buffer += chunk;
1465
+ const lines = buffer.split("\n\n");
1466
+ buffer = lines.pop() || "";
1467
+ for (const block of lines) {
1468
+ const trimmed = block.trim();
1469
+ if (!trimmed.startsWith("data:")) continue;
1470
+ const jsonStr = trimmed.replace(/^data:\s*/, "");
1471
+ try {
1472
+ const event = JSON.parse(jsonStr);
1473
+ if (event.type === "connected") {
1474
+ if (event.pendingCount > 0) {
1475
+ getRemoteOrLocalPendingJob().then((pending) => {
1476
+ if (pending) done(pending);
1477
+ });
1478
+ }
1479
+ } else if (event.type === "job_created" && event.job) {
1480
+ done(event.job);
1481
+ }
1482
+ } catch {
1483
+ }
1484
+ }
1485
+ });
1486
+ res.on("end", () => {
1487
+ if (!resolved) {
1488
+ const pending = bridgeJobStore.getPendingJob();
1489
+ if (pending) {
1490
+ done(pending);
1491
+ }
1492
+ }
1493
+ });
1494
+ res.on("error", () => {
1495
+ });
1496
+ }
1497
+ );
1498
+ req.on("error", () => {
1499
+ });
1500
+ } catch {
1501
+ }
1502
+ });
1503
+ }
1504
+ function formatJobResponse(job) {
1505
+ let instructions = "";
1506
+ let details = "";
1507
+ if (job.type === "parse_and_diagnose") {
1508
+ const p = job.payload || {};
1509
+ details = `
1510
+ - **Tipo de A\xE7\xE3o**: Extra\xE7\xE3o Multimodal e Diagn\xF3stico de Perfil (parseAndDiagnose)
1511
+ - **Cargo Almejado**: ${p.targetRole || "N\xE3o especificado"}
1512
+ - **Possui PDF Base64**: ${Boolean(p.pdfBase64)} (${p.pdfBase64 ? Math.round(p.pdfBase64.length / 1024) + " KB" : "N/A"})
1513
+ - **Possui Texto**: ${Boolean(p.pdfText)}
1514
+ `.trim();
1515
+ instructions = `
1516
+ ### Pr\xF3ximos Passos Obrigat\xF3rios:
1517
+ 1. Extraia e analise o perfil estruturado (\`Profile\`) e o diagn\xF3stico (\`ProfileReview\`) com notas (0-100), gargalos de triagem de recrutadores e lacunas de stack.
1518
+ 2. Envie o resultado de volta para o navegador chamando a tool \`submit_job_result\`:
1519
+ \`\`\`json
1520
+ {
1521
+ "jobId": "${job.id}",
1522
+ "result": {
1523
+ "profile": { ... },
1524
+ "review": { ... }
1525
+ }
1526
+ }
1527
+ \`\`\`
1528
+ 3. Ap\xF3s enviar o resultado, execute a tool \`watch_linkegringo\` novamente para aguardar a pr\xF3xima a\xE7\xE3o do usu\xE1rio (entrevista t\xE9cnica ou reescrita).
1529
+ `.trim();
1530
+ } else if (job.type === "generate_interview") {
1531
+ const p = job.payload || {};
1532
+ details = `
1533
+ - **Tipo de A\xE7\xE3o**: Gera\xE7\xE3o de Perguntas de Entrevista T\xE9cnica
1534
+ - **Candidato**: ${p.profile?.name || "N\xE3o informado"}
1535
+ - **Cargo Almejado**: ${p.objective?.primaryRole || "N\xE3o informado"}
1536
+ `.trim();
1537
+ instructions = `
1538
+ ### Pr\xF3ximos Passos Obrigat\xF3rios:
1539
+ 1. Gere as perguntas t\xE9cnicas focando nos gargalos e lacunas identificados.
1540
+ 2. Envie o resultado chamando \`submit_job_result({ jobId: "${job.id}", result: { questions: [...] } })\`.
1541
+ 3. Rearme a escuta chamando \`watch_linkegringo\` para aguardar as respostas do candidato.
1542
+ `.trim();
1543
+ } else if (job.type === "generate_rewritten_profile") {
1544
+ const p = job.payload || {};
1545
+ details = `
1546
+ - **Tipo de A\xE7\xE3o**: Reescrita Completa de Perfil com Bullets Google XYZ
1547
+ - **Candidato**: ${p.profile?.name || "N\xE3o informado"}
1548
+ - **Fatos Confirmados na Entrevista**: ${p.confirmedFacts?.length || 0}
1549
+ `.trim();
1550
+ instructions = `
1551
+ ### Pr\xF3ximos Passos Obrigat\xF3rios:
1552
+ 1. Reescreva o perfil aplicando a f\xF3rmula Google XYZ (Accomplished [X], measured by [Y], by doing [Z]) e Headline $\\le$ 160 caracteres.
1553
+ 2. Envie o resultado chamando \`submit_job_result({ jobId: "${job.id}", result: { rewritten: { ... } } })\`.
1554
+ 3. Rearme a escuta chamando \`watch_linkegringo\`.
1555
+ `.trim();
1556
+ }
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: "text",
1561
+ text: `\u{1F680} **NOVO JOB RECEBIDO DO LINKEGRINGO**: \`${job.id}\`
1562
+
1563
+ ${details}
1564
+
1565
+ ${instructions}`
1566
+ }
1567
+ ],
1568
+ structuredData: {
1569
+ jobFound: true,
1570
+ job
1571
+ }
1572
+ };
1573
+ }
1574
+ async function handleWatchLinkeGringo(input = {}) {
1575
+ const timeoutSeconds = Math.min(Math.max(input.timeoutSeconds ?? 60, 0.1), 300);
1576
+ const timeoutMs = timeoutSeconds * 1e3;
1577
+ const bridgeUrl = input.bridgeUrl || "http://127.0.0.1:5174";
1578
+ const immediateJob = await getRemoteOrLocalPendingJob();
1579
+ if (immediateJob) {
1580
+ bridgeJobStore.markProcessing(immediateJob.id);
1581
+ return formatJobResponse(immediateJob);
1582
+ }
1583
+ const job = await waitForNextJob(bridgeUrl, timeoutMs);
1584
+ if (!job) {
1585
+ return {
1586
+ content: [
1587
+ {
1588
+ type: "text",
1589
+ text: `\u{1F7E2} **Modo Watch do LinkeGringo Ativo** (Bridge porta 5174)
1590
+
1591
+ Nenhum job recebido nos \xFAltimos ${timeoutSeconds} segundos.
1592
+ A interface web est\xE1 conectada ao Bridge. Assim que o usu\xE1rio clicar em **Analisar com Agente MCP**, **Iniciar Entrevista** ou **Gerar Perfil Reescrito**, execute a tool \`watch_linkegringo\` novamente para receber e processar os dados!`
1593
+ }
1594
+ ],
1595
+ structuredData: {
1596
+ status: "waiting",
1597
+ timeoutSeconds,
1598
+ pendingCount: bridgeJobStore.getPendingCount(),
1599
+ serverOnline: true
1600
+ }
1601
+ };
1602
+ }
1603
+ bridgeJobStore.markProcessing(job.id);
1604
+ return formatJobResponse(job);
1605
+ }
1606
+
1391
1607
  // src/server.ts
1392
1608
  function createLinkeGringoMcpServer() {
1393
1609
  const server = new McpServer({
@@ -1454,6 +1670,16 @@ function createLinkeGringoMcpServer() {
1454
1670
  return await handleSubmitJobResult(args);
1455
1671
  }
1456
1672
  );
1673
+ server.registerTool(
1674
+ "watch_linkegringo",
1675
+ {
1676
+ description: "Inicia a escuta ativa por jobs enviados pela interface web do LinkeGringo (upload de perfil para diagn\xF3stico, entrevista t\xE9cnica, reescrita de perfil). Aguarda o job e o retorna imediatamente para processamento.",
1677
+ inputSchema: watchLinkeGringoInputSchema.shape
1678
+ },
1679
+ async (args) => {
1680
+ return await handleWatchLinkeGringo(args);
1681
+ }
1682
+ );
1457
1683
  server.registerResource(
1458
1684
  "guidelines",
1459
1685
  "linkegringo://guidelines",
@@ -1671,7 +1897,7 @@ function runInstaller(args = process.argv) {
1671
1897
  }
1672
1898
 
1673
1899
  // src/bridge/server.ts
1674
- import http from "http";
1900
+ import http2 from "http";
1675
1901
  function setCorsHeaders(res) {
1676
1902
  res.setHeader("Access-Control-Allow-Origin", "*");
1677
1903
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
@@ -1704,7 +1930,8 @@ async function parseBody(req) {
1704
1930
  });
1705
1931
  }
1706
1932
  function createBridgeHttpServer(options = {}) {
1707
- const server = http.createServer(async (req, res) => {
1933
+ const activeSseClients = /* @__PURE__ */ new Map();
1934
+ const server = http2.createServer(async (req, res) => {
1708
1935
  setCorsHeaders(res);
1709
1936
  if (req.method === "OPTIONS") {
1710
1937
  res.writeHead(204);
@@ -1740,11 +1967,25 @@ function createBridgeHttpServer(options = {}) {
1740
1967
  return;
1741
1968
  }
1742
1969
  if (req.method === "POST" && pathname === "/api/bridge/disconnect") {
1743
- const canceledCount = bridgeJobStore.cancelAllPending("Cancelado pelo usu\xE1rio na interface web.");
1970
+ for (const [id, client] of activeSseClients.entries()) {
1971
+ try {
1972
+ client.res.write(
1973
+ `data: ${JSON.stringify({ type: "disconnect", message: "Desconectado pelo usu\xE1rio na interface web." })}
1974
+
1975
+ `
1976
+ );
1977
+ client.res.end();
1978
+ } catch {
1979
+ }
1980
+ client.cleanup();
1981
+ }
1982
+ activeSseClients.clear();
1983
+ const { canceledCount, clearedCount } = bridgeJobStore.resetQueue();
1744
1984
  sendJson(res, 200, {
1745
1985
  ok: true,
1746
1986
  message: "Conex\xE3o cancelada e fila de jobs limpa com sucesso.",
1747
- canceledCount
1987
+ canceledCount,
1988
+ clearedCount
1748
1989
  });
1749
1990
  return;
1750
1991
  }
@@ -1784,13 +2025,16 @@ function createBridgeHttpServer(options = {}) {
1784
2025
 
1785
2026
  `);
1786
2027
  }, 15e3);
1787
- req.on("close", () => {
2028
+ const cleanup = () => {
1788
2029
  clearInterval(heartbeat);
1789
2030
  bridgeJobStore.unregisterWatcher(watcherId);
1790
2031
  bridgeJobStore.off("job:created", onJobCreated);
1791
2032
  bridgeJobStore.off("job:completed", onJobCompleted);
1792
2033
  bridgeJobStore.off("job:failed", onJobFailed);
1793
- });
2034
+ activeSseClients.delete(watcherId);
2035
+ };
2036
+ activeSseClients.set(watcherId, { res, cleanup });
2037
+ req.on("close", cleanup);
1794
2038
  return;
1795
2039
  }
1796
2040
  if (req.method === "POST" && pathname === "/api/jobs") {
@@ -1919,7 +2163,7 @@ async function stopBridgeServer() {
1919
2163
  }
1920
2164
 
1921
2165
  // src/bridge/watcher.ts
1922
- import http2 from "http";
2166
+ import http3 from "http";
1923
2167
  function startBridgeWatcher(options = {}) {
1924
2168
  const bridgeUrl = options.bridgeUrl || process.env.LINKEGRINGO_BRIDGE_URL || "http://127.0.0.1:5174";
1925
2169
  let isClosed = false;
@@ -1930,7 +2174,7 @@ function startBridgeWatcher(options = {}) {
1930
2174
  if (isClosed) return;
1931
2175
  try {
1932
2176
  const url = new URL("/api/jobs/stream", bridgeUrl);
1933
- const req = http2.get(
2177
+ const req = http3.get(
1934
2178
  url.toString(),
1935
2179
  {
1936
2180
  headers: {
@@ -1981,6 +2225,17 @@ function startBridgeWatcher(options = {}) {
1981
2225
  console.log(
1982
2226
  `[LinkeGringo Watcher] \u26A0\uFE0F Job cancelado ou com erro: ${payload.job.id} (${payload.job.error})`
1983
2227
  );
2228
+ } else if (payload.type === "disconnect") {
2229
+ console.log(
2230
+ `
2231
+ [LinkeGringo Watcher] \u{1F6D1} Conex\xE3o encerrada pelo servidor (desconectado via interface web).`
2232
+ );
2233
+ isClosed = true;
2234
+ if (currentReq) {
2235
+ currentReq.destroy();
2236
+ currentReq = null;
2237
+ }
2238
+ setTimeout(() => process.exit(0), 50);
1984
2239
  }
1985
2240
  } catch {
1986
2241
  }
@@ -2099,6 +2354,7 @@ export {
2099
2354
  handleGetPendingJob,
2100
2355
  handleSimulateRecruiterSearch,
2101
2356
  handleSubmitJobResult,
2357
+ handleWatchLinkeGringo,
2102
2358
  installMcpServerConfig,
2103
2359
  parseArgs,
2104
2360
  runInstaller,
@@ -2106,5 +2362,7 @@ export {
2106
2362
  startBridgeServer,
2107
2363
  startBridgeWatcher,
2108
2364
  stopBridgeServer,
2109
- submitJobResultInputSchema
2365
+ submitJobResultInputSchema,
2366
+ waitForNextJob,
2367
+ watchLinkeGringoInputSchema
2110
2368
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkegringo/mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Servidor MCP oficial do LinkeGringo para auditoria e otimização de perfis para o mercado internacional",
5
5
  "type": "module",
6
6
  "bin": {