@zincapp/mcp-server 1.0.1 → 1.2.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 +3 -2
- package/dist/client.js +18 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/tools/apply-completion.d.ts +3 -0
- package/dist/tools/apply-completion.js +47 -0
- package/dist/tools/audit-entities.d.ts +3 -0
- package/dist/tools/audit-entities.js +33 -0
- package/dist/tools/audit-retiradas.d.ts +3 -0
- package/dist/tools/audit-retiradas.js +36 -0
- package/dist/tools/get-retirada-audit.d.ts +3 -0
- package/dist/tools/get-retirada-audit.js +22 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/list-docs.js +4 -2
- package/dist/tools/propose-authorizations.d.ts +3 -0
- package/dist/tools/propose-authorizations.js +30 -0
- package/dist/tools/propose-completion.d.ts +3 -0
- package/dist/tools/propose-completion.js +34 -0
- package/dist/tools/propose-nt.d.ts +3 -0
- package/dist/tools/propose-nt.js +29 -0
- package/dist/tools/read-doc.js +6 -2
- package/dist/tools/search-docs.js +9 -2
- package/package.json +2 -2
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
|
-
|
|
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
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,47 @@
|
|
|
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, ATTRIBUTED TO A REAL HUMAN. The operator ' +
|
|
19
|
+
'must supply a fresh Google id-token (googleIdToken) — the write is recorded in the audit log ' +
|
|
20
|
+
'under that person, never a system user. Two internal steps: begin-session verifies Google + ' +
|
|
21
|
+
'the operator\'s write permission and mints a short-lived, single-use, batch-bound token; apply ' +
|
|
22
|
+
'then writes each edit through the framework (hooks + audit fire). Requires the RETIRADAS_WRITE ' +
|
|
23
|
+
'scope + companyId in the allowlist. Only apply edits the human has explicitly confirmed.', {
|
|
24
|
+
companyId: z.number().describe('The company (tenant) id, e.g. 9'),
|
|
25
|
+
googleIdToken: z.string().describe('A fresh Google id-token identifying the operator authorizing the write'),
|
|
26
|
+
edits: z.array(EditSchema).describe('The confirmed edits (from propose_completion), one per removal'),
|
|
27
|
+
}, async ({ companyId, googleIdToken, edits }) => {
|
|
28
|
+
// Step 1: exchange the Google id-token for a short-lived apply token bound to THIS batch.
|
|
29
|
+
const begin = await client.post('/mwm/retirada/session/begin', {
|
|
30
|
+
companyId,
|
|
31
|
+
googleIdToken,
|
|
32
|
+
edits,
|
|
33
|
+
});
|
|
34
|
+
// Step 2: apply the batch, carrying the token. The backend re-verifies the binding + human.
|
|
35
|
+
const result = await client.post('/mwm/retirada/apply', { companyId, confirm: true, edits }, { 'X-Agent-Apply-Token': begin.applyToken });
|
|
36
|
+
const ok = result.applied.length;
|
|
37
|
+
const bad = result.failed.length;
|
|
38
|
+
const failLines = bad
|
|
39
|
+
? '\n\n### Fallidos\n' + result.failed.map(f => `- Retirada ${f.removalId}: ${f.error}`).join('\n')
|
|
40
|
+
: '';
|
|
41
|
+
const text = `# Aplicado · company ${companyId}\n` +
|
|
42
|
+
`${ok} retirada(s) actualizada(s)${bad ? `, ${bad} con error` : ''}, atribuidas al operator.\n` +
|
|
43
|
+
(ok ? '\nAplicadas: ' + result.applied.join(', ') : '') +
|
|
44
|
+
failLines;
|
|
45
|
+
return { content: [{ type: 'text', text }] };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -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,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,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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/list-docs.js
CHANGED
|
@@ -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
|
|
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,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,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,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
|
+
}
|
package/dist/tools/read-doc.js
CHANGED
|
@@ -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.', {
|
|
4
|
-
|
|
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.', {
|
|
4
|
-
|
|
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
|
|
4
|
-
"description": "MCP server for ZincApp
|
|
3
|
+
"version": "1.2.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"
|