@zincapp/mcp-server 1.3.1 → 1.4.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.js CHANGED
@@ -39,7 +39,7 @@ export class ZincAppClient {
39
39
  'Authorization': `Bearer ${this.token}`,
40
40
  'Accept': 'application/json',
41
41
  'Content-Type': 'application/json',
42
- // e.g. the X-Agent-Apply-Token carrying the batch-bound, single-use human-apply token.
42
+ // Optional per-call headers (currently unused; kept for forward compatibility).
43
43
  ...(extraHeaders ?? {}),
44
44
  },
45
45
  body: body ? JSON.stringify(body) : undefined,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  // The confirmed edit shape (mirrors the backend ReconcileApplyItem). Produced from propose_completion.
3
4
  const EditSchema = z.object({
4
5
  removalId: z.number(),
@@ -18,25 +19,21 @@ export function registerApplyCompletion(server, client) {
18
19
  server.tool('apply_completion', 'Apply confirmed field completions to waste removals, UNDER YOUR ACTIVE OPERATOR ELEVATION. ' +
19
20
  'You (the platform operator) must first elevate into the company from the web (IAM) — this ' +
20
21
  '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'),
22
+ 'elevation, and if you are not elevated it asks you to do so in the web, then applies each ' +
23
+ 'confirmed edit through the framework (hooks + audit fire) as the elevated _sysadmin, ' +
24
+ 'recording the real operator in the elevation audit trail. The company is DERIVED from your ' +
25
+ 'elevation (no companyId parameter). Only apply edits you have confirmed.', {
26
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 });
27
+ }, async ({ edits }) => {
28
+ // Step 1: check the operator's current elevation (opened in the web with step-up). The
29
+ // company is whatever you elevated into — it is never passed by the tool.
30
+ const elev = await resolveElevation(client);
31
+ if ('content' in elev)
32
+ return elev;
33
+ const companyId = elev.companyId;
34
+ // Step 2: apply under the active elevation. The backend derives the company from the live
35
+ // OPEN row and re-verifies eligibility the companyId is NOT sent (server is the authority).
36
+ const result = await client.post('/mwm/retirada/apply-elevated', { confirm: true, edits });
40
37
  const ok = result.applied.length;
41
38
  const bad = result.failed.length;
42
39
  const failLines = bad
@@ -1,15 +1,20 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  export function registerAuditEntities(server, client) {
3
4
  server.tool('audit_entities', 'Audit the gestores or transportistas used by a company\'s waste removals. For each distinct ' +
4
5
  'party it checks: authorization (T01/T02 by hazard for transporters; destino+hazard role ' +
5
6
  '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'),
7
+ 'one row per party, ranked by how many removals use it. Read-only, no AI. Runs under YOUR ' +
8
+ 'active operator elevation the company is the one you elevated into from the web (no ' +
9
+ 'companyId parameter).', {
9
10
  role: z.enum(['gestor', 'transportista']).describe('Which party role to audit'),
10
11
  year: z.number().optional().describe('Restrict to removals of this year (optional)'),
11
- }, async ({ companyId, role, year }) => {
12
- const params = { companyId: String(companyId), role };
12
+ }, async ({ role, year }) => {
13
+ const elev = await resolveElevation(client);
14
+ if ('content' in elev)
15
+ return elev;
16
+ const companyId = elev.companyId;
17
+ const params = { role };
13
18
  if (year != null)
14
19
  params.year = String(year);
15
20
  const r = await client.get('/mwm/retirada/audit-entities', params);
@@ -1,14 +1,19 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  export function registerAuditRetiradas(server, client) {
3
4
  server.tool('audit_retiradas', 'Audit a company\'s waste removals (retiradas) for a year. Returns a health projection — how ' +
4
5
  'many removals are missing an NT, a transporter, documents, a residue, how many have a kg ' +
5
6
  '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'),
7
+ 'Runs under YOUR active operator elevation: the company is the one you elevated into from the ' +
8
+ 'web (no companyId parameter). If you are not elevated, it asks you to elevate first.', {
8
9
  year: z.number().describe('Calendar year, e.g. 2025'),
9
10
  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) };
11
+ }, async ({ year, limit }) => {
12
+ const elev = await resolveElevation(client);
13
+ if ('content' in elev)
14
+ return elev;
15
+ const companyId = elev.companyId;
16
+ const params = { year: String(year) };
12
17
  if (limit != null)
13
18
  params.limit = String(limit);
14
19
  const r = await client.get('/mwm/retirada/audit', params);
@@ -0,0 +1,29 @@
1
+ import type { ZincAppClient } from '../client.js';
2
+ /** The operator's current web-opened elevation, as seen through the agent token's account. */
3
+ export interface CurrentElevation {
4
+ companyId: number | null;
5
+ elevated: boolean;
6
+ }
7
+ /** A user-facing text result (the MCP tool content shape) — returned when NOT elevated. */
8
+ export type NotElevatedResult = {
9
+ content: {
10
+ type: 'text';
11
+ text: string;
12
+ }[];
13
+ };
14
+ /**
15
+ * Resolve the operator's active elevation for a retirada tool.
16
+ *
17
+ * The agent channel derives the tenant from the operator's WEB-opened elevation — no tool takes a
18
+ * companyId, and the agent token NEVER opens an elevation itself (that needs the web step-up). This
19
+ * helper hits `operator/current-elevation` and returns EITHER the elevated `companyId`, or a ready
20
+ * user-facing message telling the operator to elevate in the web first.
21
+ *
22
+ * Usage:
23
+ * const elev = await resolveElevation(client)
24
+ * if ('content' in elev) return elev // not elevated → surface the message
25
+ * const companyId = elev.companyId // proceed under this tenant
26
+ */
27
+ export declare function resolveElevation(client: ZincAppClient): Promise<{
28
+ companyId: number;
29
+ } | NotElevatedResult>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Resolve the operator's active elevation for a retirada tool.
3
+ *
4
+ * The agent channel derives the tenant from the operator's WEB-opened elevation — no tool takes a
5
+ * companyId, and the agent token NEVER opens an elevation itself (that needs the web step-up). This
6
+ * helper hits `operator/current-elevation` and returns EITHER the elevated `companyId`, or a ready
7
+ * user-facing message telling the operator to elevate in the web first.
8
+ *
9
+ * Usage:
10
+ * const elev = await resolveElevation(client)
11
+ * if ('content' in elev) return elev // not elevated → surface the message
12
+ * const companyId = elev.companyId // proceed under this tenant
13
+ */
14
+ export async function resolveElevation(client) {
15
+ const elev = await client.get('/mwm/retirada/operator/current-elevation');
16
+ if (!elev.elevated || elev.companyId == null) {
17
+ return {
18
+ content: [{
19
+ type: 'text',
20
+ text: 'No estás elevado a ningún tenant. Elévate a la company objetivo desde el portal web ' +
21
+ '(IAM → elevar) con step-up, y luego vuelve a lanzar la herramienta. El token nunca ' +
22
+ 'abre una elevación por sí mismo — actúa bajo la que tú abras en la web.',
23
+ }],
24
+ };
25
+ }
26
+ return { companyId: elev.companyId };
27
+ }
@@ -1,14 +1,17 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  export function registerGetRetiradaAudit(server, client) {
3
4
  server.tool('get_retirada_audit', 'Audit ONE waste removal (retirada) across every dimension: transporter (+ its T01/T02 ' +
4
5
  'authorization), NT, declared vs weighed kg, residue (LER), R/D operation codes, gestor ' +
5
6
  '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'),
7
+ 'mismatch plus the field that proves it. Read-only, no AI. Runs under YOUR active operator ' +
8
+ 'elevation the company is the one you elevated into from the web (no companyId parameter).', {
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) });
10
+ }, async ({ removalId }) => {
11
+ const elev = await resolveElevation(client);
12
+ if ('content' in elev)
13
+ return elev;
14
+ const a = await client.get(`/mwm/retirada/audit/${removalId}`);
12
15
  const icon = (s) => (s === 'ok' ? '✅' : s === 'mismatch' ? '⚠️' : '❌');
13
16
  const lines = a.findings.map(f => {
14
17
  const cur = f.current ?? '—';
@@ -1,15 +1,20 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  export function registerProposeAuthorizations(server, client) {
3
4
  server.tool('propose_authorizations', 'PROPOSE-ONLY: for each gestor or transportista used by the company\'s removals that lacks the ' +
4
5
  'required authorization, suggest the static authorization TYPE codes (authId) that would ' +
5
6
  'satisfy it — T01/T02 by hazard for transporters, destino+hazard role flags for gestores. ' +
6
7
  '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'),
8
+ 'registry number; the agent only proposes the type. Runs under YOUR active operator elevation ' +
9
+ ' the company is the one you elevated into from the web (no companyId parameter).', {
9
10
  role: z.enum(['gestor', 'transportista']).describe('Which party role to propose for'),
10
11
  year: z.number().optional().describe('Restrict to removals of this year (optional)'),
11
- }, async ({ companyId, role, year }) => {
12
- const params = { companyId: String(companyId), role };
12
+ }, async ({ role, year }) => {
13
+ const elev = await resolveElevation(client);
14
+ if ('content' in elev)
15
+ return elev;
16
+ const companyId = elev.companyId;
17
+ const params = { role };
13
18
  if (year != null)
14
19
  params.year = String(year);
15
20
  const r = await client.get('/mwm/retirada/propose-authorizations', params);
@@ -1,15 +1,19 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  export function registerProposeCompletion(server, client) {
3
4
  server.tool('propose_completion', 'Propose field completions for ONE waste removal by re-reading its attached documents (DIs, ' +
4
5
  'certificates) through the importer pipeline and diffing against the current DB values. Returns ' +
5
6
  'a typed diff: per field, the current value vs. the value read from the document, whether it ' +
6
7
  'resolves to a DB entity, and (for a carrier not yet in the DB) the data to create it. ' +
7
8
  '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'),
9
+ 'each cost an AI call). Runs under YOUR active operator elevation — the company is the one you ' +
10
+ 'elevated into from the web (no companyId parameter).', {
10
11
  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) });
12
+ }, async ({ removalId }) => {
13
+ const elev = await resolveElevation(client);
14
+ if ('content' in elev)
15
+ return elev;
16
+ const d = await client.get(`/mwm/retirada/propose/${removalId}`);
13
17
  if (d.changes.length === 0 && d.warnings.length === 0) {
14
18
  return { content: [{ type: 'text', text: `# Retirada ${d.removalId} — ${d.label}\n\nSin cambios propuestos (${d.docCount} doc.).` }] };
15
19
  }
@@ -1,14 +1,19 @@
1
1
  import { z } from 'zod';
2
+ import { resolveElevation } from './elevation.js';
2
3
  export function registerProposeNt(server, client) {
3
4
  server.tool('propose_nt', 'PROPOSE-ONLY: for a removal WITHOUT an NT (notificación previa), describe the contract + NT + ' +
4
5
  'NT-LER that would be created to cover it, with the create-vs-reuse dedup (existing contract ' +
5
6
  'by parties, existing NT by code) and the human-review warnings. Read-only, never writes — ' +
6
7
  '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'),
8
+ 'creates via the web UI. Runs under YOUR active operator elevation — the company is the one ' +
9
+ 'you elevated into from the web (no companyId parameter).', {
9
10
  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) });
11
+ }, async ({ removalId }) => {
12
+ const elev = await resolveElevation(client);
13
+ if ('content' in elev)
14
+ return elev;
15
+ const companyId = elev.companyId;
16
+ const p = await client.get(`/mwm/retirada/propose-nt/${removalId}`);
12
17
  if (p.alreadyHasNt) {
13
18
  return { content: [{ type: 'text', text: `La retirada ${removalId} ya tiene NT. Nada que proponer.` }] };
14
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zincapp/mcp-server",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
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": {