@zincapp/mcp-server 1.0.1 → 1.3.0

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/client.d.ts CHANGED
@@ -2,7 +2,8 @@ import type { McpConfig } from './config.js';
2
2
  export declare class ZincAppClient {
3
3
  private baseUrl;
4
4
  private token;
5
+ private locale;
5
6
  constructor(config: McpConfig);
6
- get<T>(path: string): Promise<T>;
7
- post<T>(path: string, body?: unknown): Promise<T>;
7
+ get<T>(path: string, params?: Record<string, string>): Promise<T>;
8
+ post<T>(path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
8
9
  }
package/dist/client.js CHANGED
@@ -1,13 +1,25 @@
1
1
  export class ZincAppClient {
2
2
  baseUrl;
3
3
  token;
4
+ locale;
4
5
  constructor(config) {
5
6
  this.baseUrl = config.apiBaseUrl.replace(/\/$/, '');
6
7
  this.token = config.agentToken;
8
+ this.locale = config.locale;
7
9
  }
8
- async get(path) {
9
- const url = `${this.baseUrl}/agent/v1${path}`;
10
- const res = await fetch(url, {
10
+ async get(path, params) {
11
+ const url = new URL(`${this.baseUrl}/agent/v1${path}`);
12
+ // Auto-append locale if not English
13
+ if (this.locale && this.locale !== 'en') {
14
+ url.searchParams.set('locale', this.locale);
15
+ }
16
+ // Apply per-call param overrides
17
+ if (params) {
18
+ for (const [k, v] of Object.entries(params)) {
19
+ url.searchParams.set(k, v);
20
+ }
21
+ }
22
+ const res = await fetch(url.toString(), {
11
23
  headers: {
12
24
  'Authorization': `Bearer ${this.token}`,
13
25
  'Accept': 'application/json',
@@ -19,7 +31,7 @@ export class ZincAppClient {
19
31
  }
20
32
  return res.json();
21
33
  }
22
- async post(path, body) {
34
+ async post(path, body, extraHeaders) {
23
35
  const url = `${this.baseUrl}/agent/v1${path}`;
24
36
  const res = await fetch(url, {
25
37
  method: 'POST',
@@ -27,6 +39,8 @@ export class ZincAppClient {
27
39
  'Authorization': `Bearer ${this.token}`,
28
40
  'Accept': 'application/json',
29
41
  'Content-Type': 'application/json',
42
+ // e.g. the X-Agent-Apply-Token carrying the batch-bound, single-use human-apply token.
43
+ ...(extraHeaders ?? {}),
30
44
  },
31
45
  body: body ? JSON.stringify(body) : undefined,
32
46
  });
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export interface McpConfig {
2
2
  agentToken: string;
3
3
  apiBaseUrl: string;
4
+ locale: string;
4
5
  }
5
6
  /**
6
7
  * Loads config from environment variables, then falls back to .zincapp.json
package/dist/config.js CHANGED
@@ -9,6 +9,7 @@ export function loadConfig() {
9
9
  // Environment variables take precedence
10
10
  let agentToken = process.env.ZINCAPP_AGENT_TOKEN || '';
11
11
  let apiBaseUrl = process.env.ZINCAPP_API_URL || '';
12
+ let locale = process.env.ZINCAPP_LOCALE || '';
12
13
  // Try .zincapp.json if env vars missing
13
14
  if (!agentToken) {
14
15
  const configPaths = [
@@ -23,6 +24,8 @@ export function loadConfig() {
23
24
  agentToken = parsed.agentToken;
24
25
  if (!apiBaseUrl && parsed.apiUrl)
25
26
  apiBaseUrl = parsed.apiUrl;
27
+ if (!locale && parsed.locale)
28
+ locale = parsed.locale;
26
29
  break;
27
30
  }
28
31
  catch {
@@ -37,5 +40,6 @@ export function loadConfig() {
37
40
  return {
38
41
  agentToken,
39
42
  apiBaseUrl: apiBaseUrl || 'https://api.zincapp.com/api',
43
+ locale: locale || 'en',
40
44
  };
41
45
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerApplyCompletion(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ // The confirmed edit shape (mirrors the backend ReconcileApplyItem). Produced from propose_completion.
3
+ const EditSchema = z.object({
4
+ removalId: z.number(),
5
+ fields: z.record(z.string(), z.string().nullable()).optional(),
6
+ createTransport: z
7
+ .object({
8
+ cif: z.string(),
9
+ nombre: z.string(),
10
+ nima: z.number().nullable().optional(),
11
+ direccion: z.string().nullable().optional(),
12
+ })
13
+ .nullable()
14
+ .optional(),
15
+ ntId: z.number().nullable().optional(),
16
+ });
17
+ export function registerApplyCompletion(server, client) {
18
+ server.tool('apply_completion', 'Apply confirmed field completions to waste removals, UNDER YOUR ACTIVE OPERATOR ELEVATION. ' +
19
+ 'You (the platform operator) must first elevate into the company from the web (IAM) — this ' +
20
+ 'tool NEVER opens an elevation itself (that requires the web step-up). It checks your current ' +
21
+ 'elevation, and if you are not elevated into this company it asks you to do so in the web, then ' +
22
+ 'applies each confirmed edit through the framework (hooks + audit fire) as the elevated ' +
23
+ '_sysadmin, recording the real operator in the elevation audit trail. Requires the ' +
24
+ 'RETIRADAS_WRITE scope + companyId in the token allowlist. Only apply edits you have confirmed.', {
25
+ companyId: z.number().describe('The company (tenant) id you are elevated into, e.g. 9'),
26
+ edits: z.array(EditSchema).describe('The confirmed edits (from propose_completion), one per removal'),
27
+ }, async ({ companyId, edits }) => {
28
+ // Step 1: check the operator's current elevation (opened in the web with step-up).
29
+ const elev = await client.get('/mwm/retirada/operator/current-elevation');
30
+ if (!elev.elevated) {
31
+ return { content: [{ type: 'text', text: `No estás elevado a ningún tenant. Elévate a la company ${companyId} desde el portal web ` +
32
+ `(IAM → elevar), y luego vuelve a lanzar apply_completion.` }] };
33
+ }
34
+ if (elev.companyId !== companyId) {
35
+ return { content: [{ type: 'text', text: `Estás elevado a la company ${elev.companyId}, no a la ${companyId}. Des-elévate y eleva a ` +
36
+ `la ${companyId} desde el portal web, luego vuelve a lanzar apply_completion.` }] };
37
+ }
38
+ // Step 2: apply under the active elevation. The backend re-verifies eligibility + the live OPEN.
39
+ const result = await client.post('/mwm/retirada/apply-elevated', { companyId, confirm: true, edits });
40
+ const ok = result.applied.length;
41
+ const bad = result.failed.length;
42
+ const failLines = bad
43
+ ? '\n\n### Fallidos\n' + result.failed.map(f => `- Retirada ${f.removalId}: ${f.error}`).join('\n')
44
+ : '';
45
+ const text = `# Aplicado · company ${companyId}\n` +
46
+ `${ok} retirada(s) actualizada(s)${bad ? `, ${bad} con error` : ''}, bajo tu elevación de operador.\n` +
47
+ (ok ? '\nAplicadas: ' + result.applied.join(', ') : '') +
48
+ failLines +
49
+ `\n\n_Recuerda des-elevarte desde el portal web con un motivo cuando termines._`;
50
+ return { content: [{ type: 'text', text }] };
51
+ });
52
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerAuditEntities(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ export function registerAuditEntities(server, client) {
3
+ server.tool('audit_entities', 'Audit the gestores or transportistas used by a company\'s waste removals. For each distinct ' +
4
+ 'party it checks: authorization (T01/T02 by hazard for transporters; destino+hazard role ' +
5
+ 'flags for gestores), NIMA validity (exactly 10 digits), and address plausibility. Returns ' +
6
+ 'one row per party, ranked by how many removals use it. Read-only, no AI. Requires the ' +
7
+ 'RETIRADAS_AUDIT scope + the companyId in the allowlist.', {
8
+ companyId: z.number().describe('The company (tenant) id, e.g. 9'),
9
+ role: z.enum(['gestor', 'transportista']).describe('Which party role to audit'),
10
+ year: z.number().optional().describe('Restrict to removals of this year (optional)'),
11
+ }, async ({ companyId, role, year }) => {
12
+ const params = { companyId: String(companyId), role };
13
+ if (year != null)
14
+ params.year = String(year);
15
+ const r = await client.get('/mwm/retirada/audit-entities', params);
16
+ if (r.parties.length === 0) {
17
+ return { content: [{ type: 'text', text: `Sin ${r.role}s en las retiradas de la company ${companyId}.` }] };
18
+ }
19
+ const rows = r.parties.map(p => {
20
+ const flag = (ok) => (ok ? '✅' : '❌');
21
+ const name = p.nombre ?? p.cif ?? '(sin nombre)';
22
+ const checks = `NIMA ${flag(p.nimaValid)}${p.nima ? ` (${p.nima})` : ''} · ` +
23
+ `Dir ${flag(p.direccionPlausible)} · ` +
24
+ `Autoriz ${flag(p.autorizacionPresent)}${p.autorizacion ? ` (${p.autorizacion})` : ''}`;
25
+ const gap = p.hasGaps ? '⚠️ ' : '';
26
+ return `- ${gap}**${name}** — ${p.usedByRemovals} retirada(s)${p.peligroso ? ' · peligroso' : ''}\n ${checks}`;
27
+ }).join('\n');
28
+ const gapCount = r.parties.filter(p => p.hasGaps).length;
29
+ const text = `# Auditoría ${r.role}s · company ${companyId}\n` +
30
+ `${r.parties.length} ${r.role}(s), ${gapCount} con gaps\n\n${rows}`;
31
+ return { content: [{ type: 'text', text }] };
32
+ });
33
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerAuditRetiradas(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ export function registerAuditRetiradas(server, client) {
3
+ server.tool('audit_retiradas', 'Audit a company\'s waste removals (retiradas) for a year. Returns a health projection — how ' +
4
+ 'many removals are missing an NT, a transporter, documents, a residue, how many have a kg ' +
5
+ 'mismatch or exceed NT capacity — plus a ranked list of the worst offenders. Read-only, no AI. ' +
6
+ 'The token must hold the RETIRADAS_AUDIT scope and list the companyId in its allowlist.', {
7
+ companyId: z.number().describe('The company (tenant) id to audit, e.g. 9 for La Paz'),
8
+ year: z.number().describe('Calendar year, e.g. 2025'),
9
+ limit: z.number().optional().describe('Max ranked removals to return (default 20, max 100)'),
10
+ }, async ({ companyId, year, limit }) => {
11
+ const params = { companyId: String(companyId), year: String(year) };
12
+ if (limit != null)
13
+ params.limit = String(limit);
14
+ const r = await client.get('/mwm/retirada/audit', params);
15
+ const s = r.stats;
16
+ const summary = `# Auditoría retiradas · company ${companyId} · ${s.year}\n\n` +
17
+ `**Total:** ${s.total} (${s.abiertas} abiertas)\n\n` +
18
+ `| Dimensión | Faltan |\n|---|---|\n` +
19
+ `| Sin NT | ${s.sinNt} |\n` +
20
+ `| Sin transportista | ${s.sinTransportista} |\n` +
21
+ `| Sin documentos | ${s.sinDi} |\n` +
22
+ `| Sin residuo (LER) | ${s.sinResiduo} |\n` +
23
+ `| Kg mismatch | ${s.kgMismatch} |\n` +
24
+ `| Exceso capacidad NT | ${s.ntExcesoKg} |\n`;
25
+ const ranked = r.worst.length === 0
26
+ ? '\n\n_Sin retiradas con gaps en el ranking escaneado._'
27
+ : '\n\n## Peores retiradas (por nº de gaps)\n' +
28
+ r.worst.map(w => {
29
+ const gaps = w.findings.filter(f => f.status !== 'ok')
30
+ .map(f => `${f.dimension}:${f.status}`).join(', ');
31
+ return `- **${w.removalId}** (${w.label}) — ${gaps}`;
32
+ }).join('\n');
33
+ const note = `\n\n_Escaneadas ${r.scanned} retiradas para el ranking (top ${r.ranked})._`;
34
+ return { content: [{ type: 'text', text: summary + ranked + note }] };
35
+ });
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerGetRetiradaAudit(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ export function registerGetRetiradaAudit(server, client) {
3
+ server.tool('get_retirada_audit', 'Audit ONE waste removal (retirada) across every dimension: transporter (+ its T01/T02 ' +
4
+ 'authorization), NT, declared vs weighed kg, residue (LER), R/D operation codes, gestor ' +
5
+ 'NIMA/authorization/address, and attached documents. Each check reports ok / missing / ' +
6
+ 'mismatch plus the field that proves it. Read-only, no AI. The token must hold RETIRADAS_AUDIT ' +
7
+ 'and list the companyId.', {
8
+ companyId: z.number().describe('The company (tenant) id, e.g. 9'),
9
+ removalId: z.number().describe('The removal id to audit'),
10
+ }, async ({ companyId, removalId }) => {
11
+ const a = await client.get(`/mwm/retirada/audit/${removalId}`, { companyId: String(companyId) });
12
+ const icon = (s) => (s === 'ok' ? '✅' : s === 'mismatch' ? '⚠️' : '❌');
13
+ const lines = a.findings.map(f => {
14
+ const cur = f.current ?? '—';
15
+ const exp = f.expected != null ? ` → esperado: ${f.expected}` : '';
16
+ return `${icon(f.status)} **${f.dimension}**: ${f.status} (actual: ${cur}${exp}) · _${f.provingField}_`;
17
+ }).join('\n');
18
+ const text = `# Retirada ${a.removalId} — ${a.label}\n` +
19
+ `${a.hasGaps ? '**Tiene gaps.**' : '**Completa.**'}\n\n${lines}`;
20
+ return { content: [{ type: 'text', text }] };
21
+ });
22
+ }
@@ -5,6 +5,13 @@ import { registerListEndpoints } from './list-endpoints.js';
5
5
  import { registerGetEndpoint } from './get-endpoint.js';
6
6
  import { registerSandboxRequest } from './sandbox-request.js';
7
7
  import { registerGetContract } from './get-contract.js';
8
+ import { registerAuditRetiradas } from './audit-retiradas.js';
9
+ import { registerGetRetiradaAudit } from './get-retirada-audit.js';
10
+ import { registerProposeCompletion } from './propose-completion.js';
11
+ import { registerAuditEntities } from './audit-entities.js';
12
+ import { registerApplyCompletion } from './apply-completion.js';
13
+ import { registerProposeAuthorizations } from './propose-authorizations.js';
14
+ import { registerProposeNt } from './propose-nt.js';
8
15
  export function registerAllTools(server, client) {
9
16
  registerSearchDocs(server, client);
10
17
  registerReadDoc(server, client);
@@ -13,4 +20,11 @@ export function registerAllTools(server, client) {
13
20
  registerGetEndpoint(server, client);
14
21
  registerSandboxRequest(server, client);
15
22
  registerGetContract(server, client);
23
+ registerAuditRetiradas(server, client);
24
+ registerGetRetiradaAudit(server, client);
25
+ registerProposeCompletion(server, client);
26
+ registerAuditEntities(server, client);
27
+ registerApplyCompletion(server, client);
28
+ registerProposeAuthorizations(server, client);
29
+ registerProposeNt(server, client);
16
30
  }
@@ -1,6 +1,8 @@
1
+ import { z } from 'zod';
1
2
  export function registerListDocs(server, client) {
2
- server.tool('list_docs', 'List all available ZincApp developer documentation with titles and summaries.', {}, async () => {
3
- const docs = await client.get('/docs?limit=200');
3
+ server.tool('list_docs', 'List all available ZincApp developer documentation with titles and summaries.', { locale: z.enum(['en', 'es']).optional().describe('Language locale (default: config locale)') }, async ({ locale }) => {
4
+ const params = locale ? { locale } : undefined;
5
+ const docs = await client.get('/docs?limit=200', params);
4
6
  const text = docs.length === 0
5
7
  ? 'No documentation available.'
6
8
  : docs.map(d => `- **${d.title}** (\`${d.id}\`): ${d.aiSummary || d.description}`).join('\n');
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerProposeAuthorizations(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+ export function registerProposeAuthorizations(server, client) {
3
+ server.tool('propose_authorizations', 'PROPOSE-ONLY: for each gestor or transportista used by the company\'s removals that lacks the ' +
4
+ 'required authorization, suggest the static authorization TYPE codes (authId) that would ' +
5
+ 'satisfy it — T01/T02 by hazard for transporters, destino+hazard role flags for gestores. ' +
6
+ 'Read-only, never writes. A human still creates the authorization and supplies the real ' +
7
+ 'registry number; the agent only proposes the type. Requires the RETIRADAS_AUDIT scope.', {
8
+ companyId: z.number().describe('The company (tenant) id, e.g. 9'),
9
+ role: z.enum(['gestor', 'transportista']).describe('Which party role to propose for'),
10
+ year: z.number().optional().describe('Restrict to removals of this year (optional)'),
11
+ }, async ({ companyId, role, year }) => {
12
+ const params = { companyId: String(companyId), role };
13
+ if (year != null)
14
+ params.year = String(year);
15
+ const r = await client.get('/mwm/retirada/propose-authorizations', params);
16
+ if (r.proposals.length === 0) {
17
+ return { content: [{ type: 'text', text: `Ningún ${r.role} sin autorización en la company ${companyId}.` }] };
18
+ }
19
+ const blocks = r.proposals.map(p => {
20
+ const name = p.nombre ?? p.cif ?? '(sin nombre)';
21
+ const cands = p.candidates.length
22
+ ? p.candidates.map(c => ` - \`${c.authId}\`${c.name ? ` — ${c.name}` : ''}`).join('\n')
23
+ : ' _(sin candidatos en el catálogo)_';
24
+ const warns = p.warnings.map(w => ` ⚠️ ${w}`).join('\n');
25
+ return `### ${name} — ${p.usedByRemovals} retirada(s)${p.peligroso ? ' · peligroso' : ''}\nCandidatos (authId):\n${cands}\n${warns}`;
26
+ }).join('\n\n');
27
+ const text = `# Propuesta de autorizaciones · ${r.role}s · company ${companyId}\n${r.proposals.length} sin autorización\n\n${blocks}`;
28
+ return { content: [{ type: 'text', text }] };
29
+ });
30
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerProposeCompletion(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export function registerProposeCompletion(server, client) {
3
+ server.tool('propose_completion', 'Propose field completions for ONE waste removal by re-reading its attached documents (DIs, ' +
4
+ 'certificates) through the importer pipeline and diffing against the current DB values. Returns ' +
5
+ 'a typed diff: per field, the current value vs. the value read from the document, whether it ' +
6
+ 'resolves to a DB entity, and (for a carrier not yet in the DB) the data to create it. ' +
7
+ 'READ-ONLY — proposes only, never writes. One removal per call (cost control: scanned/CDO docs ' +
8
+ 'each cost an AI call). Requires RETIRADAS_AUDIT scope + the companyId in the allowlist.', {
9
+ companyId: z.number().describe('The company (tenant) id, e.g. 9'),
10
+ removalId: z.number().describe('The removal id to propose completions for'),
11
+ }, async ({ companyId, removalId }) => {
12
+ const d = await client.get(`/mwm/retirada/propose/${removalId}`, { companyId: String(companyId) });
13
+ if (d.changes.length === 0 && d.warnings.length === 0) {
14
+ return { content: [{ type: 'text', text: `# Retirada ${d.removalId} — ${d.label}\n\nSin cambios propuestos (${d.docCount} doc.).` }] };
15
+ }
16
+ const changeLines = d.changes.map(c => {
17
+ const cur = c.currentDisplay ?? c.currentValue ?? '—';
18
+ const prop = c.proposedDisplay ?? c.proposedValue ?? '—';
19
+ const create = c.createEntity
20
+ ? ` · **CREAR entidad** ${c.createEntity.nombre} (CIF ${c.createEntity.cif}${c.createEntity.nima ? `, NIMA ${c.createEntity.nima}` : ''})`
21
+ : '';
22
+ const unresolved = c.resolvable || c.createEntity ? '' : ' _(no resuelve a entidad)_';
23
+ return `- **${c.label}** (\`${c.field}\`): ${cur} → **${prop}**${create}${unresolved}`;
24
+ }).join('\n');
25
+ const warnLines = d.warnings.length
26
+ ? '\n\n### Avisos\n' + d.warnings.map(w => `- ⚠️ ${w}`).join('\n')
27
+ : '';
28
+ const text = `# Retirada ${d.removalId} — ${d.label}\n` +
29
+ `${d.docCount} documento(s) · ${d.changes.length} cambio(s) propuesto(s)\n\n` +
30
+ changeLines + warnLines +
31
+ `\n\n_Propuesta read-only. Aplicar requerirá confirmación humana (fase de escritura)._`;
32
+ return { content: [{ type: 'text', text }] };
33
+ });
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ZincAppClient } from '../client.js';
3
+ export declare function registerProposeNt(server: McpServer, client: ZincAppClient): void;
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export function registerProposeNt(server, client) {
3
+ server.tool('propose_nt', 'PROPOSE-ONLY: for a removal WITHOUT an NT (notificación previa), describe the contract + NT + ' +
4
+ 'NT-LER that would be created to cover it, with the create-vs-reuse dedup (existing contract ' +
5
+ 'by parties, existing NT by code) and the human-review warnings. Read-only, never writes — ' +
6
+ 'creating an NT is high-risk (wrong contract, duplicate, capacity), so a human confirms and ' +
7
+ 'creates via the web UI. Requires the RETIRADAS_AUDIT scope.', {
8
+ companyId: z.number().describe('The company (tenant) id, e.g. 9'),
9
+ removalId: z.number().describe('The removal id (must have no NT)'),
10
+ }, async ({ companyId, removalId }) => {
11
+ const p = await client.get(`/mwm/retirada/propose-nt/${removalId}`, { companyId: String(companyId) });
12
+ if (p.alreadyHasNt) {
13
+ return { content: [{ type: 'text', text: `La retirada ${removalId} ya tiene NT. Nada que proponer.` }] };
14
+ }
15
+ const contract = p.reuseContractId != null
16
+ ? `Contrato: **reusar** el existente (ctId ${p.reuseContractId})`
17
+ : `Contrato: **crear uno nuevo** (no existe para estas partes)`;
18
+ const parties = `Partes — origen: ${p.parties.originId}/${p.parties.originCenterId} · ` +
19
+ `operador/destino: ${p.parties.destId}/${p.parties.destCenterId}`;
20
+ const residuo = `Residuo: LER ${p.codigoLer ?? '?'} (residueId ${p.residueId ?? '?'}` +
21
+ `${p.existingCtLerResidueId != null ? `, reusar residueId ${p.existingCtLerResidueId} del CT_LER` : ''})`;
22
+ const cantidad = `Cantidad NT propuesta: ${p.proposedCantidad ?? '(desconocida)'}`;
23
+ const warns = p.warnings.map(w => `- ⚠️ ${w}`).join('\n');
24
+ const text = `# Propuesta de NT · retirada ${p.removalId} · company ${companyId}\n\n` +
25
+ `${contract}\n${parties}\n${residuo}\n${cantidad}\n\n### Avisos de revisión\n${warns}\n\n` +
26
+ `_Propuesta read-only. La creación la confirma y ejecuta una persona (por el framework)._`;
27
+ return { content: [{ type: 'text', text }] };
28
+ });
29
+ }
@@ -1,7 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  export function registerReadDoc(server, client) {
3
- server.tool('read_doc', 'Read a specific ZincApp documentation page by its ID. Returns the full markdown content.', { docId: z.string().describe('Document ID (e.g. "api.overview")') }, async ({ docId }) => {
4
- const doc = await client.get(`/docs/${encodeURIComponent(docId)}`);
3
+ server.tool('read_doc', 'Read a specific ZincApp documentation page by its ID. Returns the full markdown content.', {
4
+ docId: z.string().describe('Document ID (e.g. "api.overview")'),
5
+ locale: z.enum(['en', 'es']).optional().describe('Language locale (default: config locale)'),
6
+ }, async ({ docId, locale }) => {
7
+ const params = locale ? { locale } : undefined;
8
+ const doc = await client.get(`/docs/${encodeURIComponent(docId)}`, params);
5
9
  const text = `# ${doc.title}\n\n${doc.markdown}`;
6
10
  return { content: [{ type: 'text', text }] };
7
11
  });
@@ -1,7 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  export function registerSearchDocs(server, client) {
3
- server.tool('search_docs', 'Search ZincApp developer documentation by keyword. Returns matching documents ranked by relevance.', { query: z.string().describe('Search query'), limit: z.number().optional().describe('Max results (default 10)') }, async ({ query, limit }) => {
4
- const results = await client.get(`/docs/search?q=${encodeURIComponent(query)}&limit=${limit || 10}`);
3
+ server.tool('search_docs', 'Search ZincApp developer documentation by keyword. Returns matching documents ranked by relevance.', {
4
+ query: z.string().describe('Search query'),
5
+ limit: z.number().optional().describe('Max results (default 10)'),
6
+ locale: z.enum(['en', 'es']).optional().describe('Language locale (default: config locale)'),
7
+ }, async ({ query, limit, locale }) => {
8
+ const params = {};
9
+ if (locale)
10
+ params.locale = locale;
11
+ const results = await client.get(`/docs/search?q=${encodeURIComponent(query)}&limit=${limit || 10}`, Object.keys(params).length > 0 ? params : undefined);
5
12
  const text = results.length === 0
6
13
  ? 'No documents found matching your query.'
7
14
  : results.map(r => `## ${r.title}\n- ID: \`${r.id}\`\n- ${r.aiSummary || r.description}\n- Relevance: ${r.relevance.toFixed(2)}`).join('\n\n');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zincapp/mcp-server",
3
- "version": "1.0.1",
4
- "description": "MCP server for ZincApp Developer Portal enables AI coding assistants to search docs, read API reference, and execute sandbox API calls",
3
+ "version": "1.3.0",
4
+ "description": "MCP server for ZincApp — search docs, read API reference, execute sandbox calls, and audit/complete waste removals (retiradas)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "zincapp-mcp-server": "./dist/index.js"