@pharmadate/mcp 0.0.0-stage → 0.1.1

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -2
  3. package/dist/auth/crypto.d.ts +24 -0
  4. package/dist/auth/crypto.js +110 -0
  5. package/dist/auth/login-page.d.ts +32 -0
  6. package/dist/auth/login-page.js +98 -0
  7. package/dist/auth/oauth-provider.d.ts +92 -0
  8. package/dist/auth/oauth-provider.js +208 -0
  9. package/dist/auth/redirect-trust.d.ts +26 -0
  10. package/dist/auth/redirect-trust.js +94 -0
  11. package/dist/client.d.ts +54 -0
  12. package/dist/client.js +131 -0
  13. package/dist/config.d.ts +16 -0
  14. package/dist/config.js +28 -0
  15. package/dist/http.d.ts +79 -0
  16. package/dist/http.js +272 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +114 -0
  19. package/dist/limits.d.ts +74 -0
  20. package/dist/limits.js +152 -0
  21. package/dist/logger.d.ts +10 -0
  22. package/dist/logger.js +34 -0
  23. package/dist/operation-mode.d.ts +14 -0
  24. package/dist/operation-mode.js +10 -0
  25. package/dist/pharmacy-resolver.d.ts +54 -0
  26. package/dist/pharmacy-resolver.js +92 -0
  27. package/dist/principal.d.ts +30 -0
  28. package/dist/principal.js +85 -0
  29. package/dist/prompts.d.ts +3 -0
  30. package/dist/prompts.js +42 -0
  31. package/dist/resources.d.ts +15 -0
  32. package/dist/resources.js +58 -0
  33. package/dist/results.d.ts +42 -0
  34. package/dist/results.js +121 -0
  35. package/dist/scrub.d.ts +18 -0
  36. package/dist/scrub.js +49 -0
  37. package/dist/server-instructions.d.ts +8 -0
  38. package/dist/server-instructions.js +39 -0
  39. package/dist/server.d.ts +31 -0
  40. package/dist/server.js +59 -0
  41. package/dist/tools/common.d.ts +9 -0
  42. package/dist/tools/common.js +225 -0
  43. package/dist/tools/index.d.ts +15 -0
  44. package/dist/tools/index.js +6 -0
  45. package/dist/tools/operate.d.ts +15 -0
  46. package/dist/tools/operate.js +24 -0
  47. package/dist/tools/partner.d.ts +2 -0
  48. package/dist/tools/partner.js +176 -0
  49. package/dist/tools/types.d.ts +39 -0
  50. package/dist/tools/types.js +3 -0
  51. package/dist/version.d.ts +1 -0
  52. package/dist/version.js +15 -0
  53. package/package.json +75 -4
  54. package/server.json +56 -0
package/dist/scrub.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Redaccion de credenciales.
3
+ *
4
+ * Ninguna API key ni token del MCP puede acabar en un log, en un mensaje de error ni en un resultado de herramienta
5
+ * (salvo la key que el usuario ha pedido ver con `reveal_api_key: true`, que sale por su propio bloque).
6
+ *
7
+ * @module scrub
8
+ */
9
+ /** API keys de Pharmadate: `pk_live_`, `pk_partner_`, `pk_test_` y `pk_admin_` seguidas de hex. */
10
+ const KEY_RE = /pk_(live|partner|test|admin)_[0-9a-fA-F]{16,}/g;
11
+ /** Tokens que emite el OAuth del propio MCP (`pda1.` + base64url). */
12
+ const TOKEN_RE = /pd[acr]1\.[A-Za-z0-9_-]{16,}/g;
13
+ /** `Bearer <algo>` suelto en un texto. */
14
+ const BEARER_RE = /(Bearer\s+)[A-Za-z0-9._~+/=-]{12,}/gi;
15
+ /** Sustituye las credenciales de un texto por marcas inofensivas. */
16
+ export function scrubText(texto) {
17
+ return texto
18
+ .replace(KEY_RE, 'pk_$1_[REDACTED]')
19
+ .replace(TOKEN_RE, '[REDACTED_TOKEN]')
20
+ .replace(BEARER_RE, '$1[REDACTED]');
21
+ }
22
+ /** Un `Error` como objeto plano y redactado: sus campos no son enumerables y `Object.entries(err)` daba `{}` (se perdia el motivo). */
23
+ export function scrubError(err) {
24
+ const salida = { name: err.name, message: scrubText(err.message) };
25
+ if (typeof err.stack === 'string')
26
+ salida.stack = scrubText(err.stack);
27
+ return salida;
28
+ }
29
+ /** Igual que {@link scrubText} pero recorre objetos y listas. No muta el original. Los `Error` salen como `{name, message, stack}`. */
30
+ export function scrubDeep(valor, profundidad = 0) {
31
+ if (valor instanceof Error)
32
+ return scrubError(valor);
33
+ if (profundidad > 12)
34
+ return valor;
35
+ if (typeof valor === 'string')
36
+ return scrubText(valor);
37
+ if (Array.isArray(valor))
38
+ return valor.map((v) => scrubDeep(v, profundidad + 1));
39
+ if (valor && typeof valor === 'object') {
40
+ const salida = {};
41
+ for (const [k, v] of Object.entries(valor)) {
42
+ salida[k] = /^(authorization|api_key|apikey|x-api-key|access_token|refresh_token|client_secret)$/i.test(k)
43
+ ? '[REDACTED]'
44
+ : scrubDeep(v, profundidad + 1);
45
+ }
46
+ return salida;
47
+ }
48
+ return valor;
49
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Instrucciones del servidor: el modelo mental que recibe el LLM al conectar. Se mantienen cortas; el detalle esta en
3
+ * los resources (`pharmadate://docs/...`) y en las descripciones de cada herramienta.
4
+ *
5
+ * @module server-instructions
6
+ */
7
+ import type { Role } from './principal.js';
8
+ export declare function buildInstructions(role: Role): string;
@@ -0,0 +1,39 @@
1
+ export function buildInstructions(role) {
2
+ const comun = `
3
+ # Pharmadate MCP
4
+
5
+ Pharmadate es la plataforma de pedidos de farmacias a laboratorios y almacen. Este servidor expone su API v1; el nombre de cada herramienta es el \`operationId\` del OpenAPI.
6
+
7
+ ## Flujo de un pedido
8
+ 1. **getCatalog** (con \`q\`) para localizar los productos y su codigo nacional (CN). El precio del catalogo es orientativo.
9
+ 2. **createQuote** SIEMPRE antes de pedir: ensena al usuario el resumen (total, lineas no servibles, si llega al pedido minimo).
10
+ 3. **createOrder** solo tras la confirmacion EXPLICITA del usuario. Es un pedido REAL y firme.
11
+ - \`external_ref\` es obligatoria y es la clave de idempotencia: inventala UNA vez y REUTILIZALA en cualquier reintento.
12
+ - Si la llamada se corta, NO la repitas con otra ref: reintenta con la misma o consulta listOrders con \`external_ref\`.
13
+ 4. **listOrders / getOrder / listOrderShipments / getOrderModification** para seguir el pedido (estados, envios, faltas).
14
+
15
+ ## Reglas
16
+ - Nunca inventes codigos nacionales, ids, CIF ni referencias: salen de las herramientas o del usuario.
17
+ - Pide confirmacion antes de cualquier escritura. Las destructivas (baja, rotar key, modificar) llevan \`destructiveHint\`.
18
+ - Un error de la herramienta (\`isError\`) trae la accion siguiente en el texto: sigue esa instruccion en vez de reintentar a ciegas.
19
+ - Las API keys son secretos. No las repitas ni las guardes; \`reveal_api_key: true\` las entrega al usuario en un bloque aparte, pero pueden quedar en el historial del chat.
20
+
21
+ ## Errores frecuentes -> accion
22
+ - \`external_ref_conflict\`: la ref ya existe con otra cesta -> getOrder del \`order_id\` o usa otra ref nueva (con confirmacion).
23
+ - \`below_minimum_order\` (422): no llega al minimo -> ensena \`order_minimum\`; no se creo nada.
24
+ - \`pharmacy_inactive\`: farmacia de baja -> reactivatePharmacy.
25
+ - \`pharmacy_active\` (409 al reactivar): ya esta activa -> rotatePharmacyApiKey si se necesita una key nueva.
26
+ `;
27
+ const partner = `
28
+ ## Eres un PARTNER
29
+ Las herramientas operativas (getConditions, lookupConditions, createQuote, createOrder, listOrders, getOrder, getOrderModification, listOrderShipments, getOrderShipment, getShipment) piden el argumento **pharmacy**: \`{ pharmacy_id }\` (preferido) o \`{ cif }\`.
30
+ - La farmacia se COMPRUEBA antes de operar. Si no es una de las tuyas y activa, no se hace nada: este MCP nunca da de alta una farmacia por error.
31
+ - Para una farmacia nueva: **listPharmacies** (comprueba el CIF), luego **createPharmacy** con confirmacion del usuario.
32
+ - Gestion de farmacias: listPharmacies, getPharmacy, createPharmacy, updatePharmacy, deactivatePharmacy (se deshace con reactivatePharmacy), reactivatePharmacy, rotatePharmacyApiKey (invalida la anterior en el acto).
33
+ `;
34
+ const farmacia = `
35
+ ## Eres una FARMACIA
36
+ Tu API key ya identifica a la farmacia: no hay que indicar ninguna. Solo ves y pides lo tuyo. Las herramientas de gestion de farmacias son solo para partners.
37
+ `;
38
+ return (comun + (role === 'partner' ? partner : farmacia)).trim();
39
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Fabrica del servidor MCP: `createPharmadateServer({ principal, client })`.
3
+ *
4
+ * Se crea UNO por peticion (HTTP stateless) o uno por proceso (stdio), siempre atado a la credencial del cliente. No
5
+ * guarda ningun estado de negocio: la autorizacion la hace la API en cada llamada.
6
+ *
7
+ * @module server
8
+ */
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import type { PharmadateClient } from './client.js';
11
+ import type { OperationMode } from './config.js';
12
+ import { credentialId, type Principal } from './principal.js';
13
+ import type { PharmacyRefArg } from './pharmacy-resolver.js';
14
+ import { McpLimits } from './limits.js';
15
+ import { type ToolDef } from './tools/index.js';
16
+ export interface CreateServerOptions {
17
+ principal: Principal;
18
+ client: PharmadateClient;
19
+ /** Hash de la credencial (clave de los limites). */
20
+ credentialId: string;
21
+ mode: OperationMode;
22
+ limits: McpLimits;
23
+ /** stdio: el cliente MCP ya esta conectado y su nombre va al User-Agent. */
24
+ transport?: 'stdio' | 'remote';
25
+ /** Farmacia por defecto (PHARMADATE_PHARMACY_REF), solo partners. */
26
+ defaultPharmacy?: PharmacyRefArg | undefined;
27
+ }
28
+ /** Herramientas que se registran para un rol y modo (en READ_ONLY no se ofrecen las de escritura). */
29
+ export declare function selectTools(principal: Principal, mode: OperationMode, defaultPharmacy?: boolean): ToolDef[];
30
+ export declare function createPharmadateServer(options: CreateServerOptions): McpServer;
31
+ export { credentialId };
package/dist/server.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Fabrica del servidor MCP: `createPharmadateServer({ principal, client })`.
3
+ *
4
+ * Se crea UNO por peticion (HTTP stateless) o uno por proceso (stdio), siempre atado a la credencial del cliente. No
5
+ * guarda ningun estado de negocio: la autorizacion la hace la API en cada llamada.
6
+ *
7
+ * @module server
8
+ */
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { credentialId } from './principal.js';
11
+ import { buildUserAgent } from './client.js';
12
+ import { deniedMessage, isAllowed } from './operation-mode.js';
13
+ import { errResult } from './results.js';
14
+ import { buildInstructions } from './server-instructions.js';
15
+ import { registerResources } from './resources.js';
16
+ import { registerPrompts } from './prompts.js';
17
+ import { toolsForRole } from './tools/index.js';
18
+ import { logger } from './logger.js';
19
+ import { VERSION } from './version.js';
20
+ /** Herramientas que se registran para un rol y modo (en READ_ONLY no se ofrecen las de escritura). */
21
+ export function selectTools(principal, mode, defaultPharmacy = false) {
22
+ return toolsForRole(principal.role, { defaultPharmacy }).filter((t) => isAllowed(mode, t.op));
23
+ }
24
+ export function createPharmadateServer(options) {
25
+ const { principal, client, mode, limits } = options;
26
+ const server = new McpServer({ name: 'pharmadate-mcp', title: 'Pharmadate', version: VERSION }, { instructions: buildInstructions(principal.role), capabilities: { logging: {} } });
27
+ for (const tool of selectTools(principal, mode, options.defaultPharmacy !== undefined && principal.role === 'partner')) {
28
+ server.registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema: tool.inputSchema, annotations: { title: tool.title, ...tool.annotations } }, async (args) => {
29
+ const t0 = Date.now();
30
+ if (!isAllowed(mode, tool.op))
31
+ return errResult(deniedMessage(tool.name, mode), { error_code: 'operation_not_permitted' });
32
+ if (tool.op !== 'READ') {
33
+ const rechazo = limits.check(options.credentialId, tool.name === 'createOrder');
34
+ if (rechazo)
35
+ return errResult(rechazo, { error_code: 'write_rate_limit_exceeded' });
36
+ }
37
+ try {
38
+ const result = await tool.handler(args, { client, principal, defaultPharmacy: options.defaultPharmacy });
39
+ logger.info({ tool: tool.name, role: principal.role, isError: result.isError === true, ms: Date.now() - t0, request_id: result._meta?.['request_id'] }, 'tool call');
40
+ return result;
41
+ }
42
+ catch (err) {
43
+ logger.error({ err, tool: tool.name }, 'tool call failed');
44
+ return errResult(`Error interno del MCP al ejecutar ${tool.name}. No se ha confirmado que la operacion se ejecutara.`, { error_code: 'mcp_internal_error' });
45
+ }
46
+ });
47
+ }
48
+ if (options.transport === 'stdio') {
49
+ server.server.oninitialized = () => {
50
+ const info = server.server.getClientVersion();
51
+ if (info)
52
+ client.setUserAgent(buildUserAgent(VERSION, 'stdio', `${info.name}/${info.version}`));
53
+ };
54
+ }
55
+ registerResources(server, client, principal);
56
+ registerPrompts(server, principal.role);
57
+ return server;
58
+ }
59
+ export { credentialId };
@@ -0,0 +1,9 @@
1
+ import type { Role } from '../principal.js';
2
+ import { type ToolDef } from './types.js';
3
+ /**
4
+ * Construye las herramientas comunes para un rol: solo el partner ve el argumento `pharmacy` (opcional si hay una farmacia
5
+ * por defecto configurada, `PHARMADATE_PHARMACY_REF`).
6
+ */
7
+ export declare function commonTools(role: Role, opts?: {
8
+ defaultPharmacy?: boolean;
9
+ }): ToolDef[];
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Herramientas comunes: las 12 operaciones de catalogo, condiciones, valoracion, pedidos y envios.
3
+ * Las usa una farmacia (con su key) o un partner (con el argumento `pharmacy`).
4
+ *
5
+ * @module tools/common
6
+ */
7
+ import { z } from 'zod';
8
+ import { LONG_TIMEOUT_MS } from '../client.js';
9
+ import { MAX_LINES } from '../limits.js';
10
+ import { pharmacyRefShape } from '../pharmacy-resolver.js';
11
+ import { apiErrorResult, networkErrorResult, okResult } from '../results.js';
12
+ import { ApiNetworkError } from '../client.js';
13
+ import { operate } from './operate.js';
14
+ import { BOTH_ROLES, READ_ANN } from './types.js';
15
+ const LINEA = z.object({
16
+ cn: z.string().min(1).max(20).describe('Codigo nacional (CN) del producto. Solo cuentan los digitos.'),
17
+ unidades: z.number().int().min(1).max(99_999_999).describe('Unidades (entero >= 1).'),
18
+ descripcion: z.string().max(255).optional().describe('Opcional, solo informativo.'),
19
+ });
20
+ const ITEMS = z
21
+ .array(LINEA)
22
+ .min(1)
23
+ .max(MAX_LINES)
24
+ .describe(`Lineas de la cesta (entre 1 y ${MAX_LINES}). Un mismo CN repetido se fusiona.`);
25
+ /** Un CN que va en la ruta (`/conditions/{cn}`): solo digitos. Se comprueba aqui y no se confia en `encodeURIComponent`. */
26
+ const CN_RUTA = z.string().regex(/^\d{1,20}$/, 'Un codigo nacional son solo digitos (hasta 20).');
27
+ const ORDER_ID = z.number().int().positive().describe('`order_id` del pedido en Pharmadate.');
28
+ const SHIPMENT_ID = z.string().max(100).regex(/^shp_[\w-]+$/, 'Un shipment_id tiene la forma shp_... (letras, digitos, _ y -).').describe('Identificador del envio (`shp_...`, opaco y global).');
29
+ const PAGE = z.number().int().min(1).optional().describe('Pagina (empieza en 1).');
30
+ /**
31
+ * Construye las herramientas comunes para un rol: solo el partner ve el argumento `pharmacy` (opcional si hay una farmacia
32
+ * por defecto configurada, `PHARMADATE_PHARMACY_REF`).
33
+ */
34
+ export function commonTools(role, opts = {}) {
35
+ const farmacia = role === 'partner' ? { pharmacy: opts.defaultPharmacy ? pharmacyRefShape.optional() : pharmacyRefShape } : {};
36
+ const aviso = role === 'partner' ? ' Con key de partner indica `pharmacy` (la farmacia se comprueba antes de operar).' : '';
37
+ return [
38
+ {
39
+ name: 'ping',
40
+ title: 'Comprobar la conexion',
41
+ description: 'Comprueba que la credencial es valida y dice quien eres: devuelve el partner o la farmacia asociados y la version de la API. No tiene efectos. Util para empezar.',
42
+ op: 'READ',
43
+ http: { method: 'GET', path: '/api/v1/ping' },
44
+ roles: BOTH_ROLES,
45
+ annotations: READ_ANN,
46
+ inputSchema: {},
47
+ handler: async (_args, ctx) => {
48
+ try {
49
+ const res = await ctx.client.request({ method: 'GET', path: '/v1/ping' });
50
+ if (!res.ok)
51
+ return apiErrorResult(res);
52
+ return okResult({ role: ctx.principal.role, ...res.body }, { request_id: res.requestId, http_status: res.status });
53
+ }
54
+ catch (err) {
55
+ if (err instanceof ApiNetworkError)
56
+ return networkErrorResult(err);
57
+ throw err;
58
+ }
59
+ },
60
+ },
61
+ {
62
+ name: 'getCatalog',
63
+ title: 'Buscar en el catalogo',
64
+ description: 'Catalogo de Pharmadate (la matriz y las referencias que solo existen dentro de una oferta, con `in_matrix: false`). Usa `q` para buscar por nombre o laboratorio (p. ej. q="ibuprofeno 600"). ' +
65
+ 'El precio (`list_price_eur`) es ORIENTATIVO: el bueno lo da createQuote. No hace falta indicar farmacia. Por defecto 50 por pagina.',
66
+ op: 'READ',
67
+ http: { method: 'GET', path: '/api/v1/catalog' },
68
+ roles: BOTH_ROLES,
69
+ annotations: READ_ANN,
70
+ inputSchema: {
71
+ q: z.string().min(2).max(100).optional().describe('Busqueda por nombre: cada palabra debe aparecer en la descripcion o el laboratorio, sin distinguir mayusculas ni tildes.'),
72
+ since: z.string().max(40).optional().describe('Solo lo modificado despues de esta fecha ISO 8601 (el `as_of` de una pasada anterior).'),
73
+ page: PAGE,
74
+ per_page: z.number().int().min(1).max(500).optional().describe('Resultados por pagina (maximo 500, por defecto 50).'),
75
+ },
76
+ handler: async (args, ctx) => operate(ctx, null, {
77
+ method: 'GET',
78
+ path: '/v1/catalog',
79
+ query: { q: args.q, since: args.since, page: args.page, per_page: args.per_page ?? 50 },
80
+ }),
81
+ },
82
+ {
83
+ name: 'getConditions',
84
+ title: 'Condiciones de un producto',
85
+ description: `Ofertas y condiciones comerciales vigentes de UN codigo nacional (precio, descuento, minimos y escalones).${aviso}`,
86
+ op: 'READ',
87
+ http: { method: 'GET', path: '/api/v1/conditions/{cn}' },
88
+ roles: BOTH_ROLES,
89
+ annotations: READ_ANN,
90
+ inputSchema: { ...farmacia, cn: CN_RUTA.describe('Codigo nacional (solo digitos).') },
91
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'GET', path: `/v1/conditions/${encodeURIComponent(args.cn)}` }),
92
+ },
93
+ {
94
+ name: 'lookupConditions',
95
+ title: 'Condiciones de varios productos',
96
+ description: `Consulta masiva de condiciones (hasta 1000 codigos nacionales). No crea nada.${aviso}`,
97
+ op: 'READ',
98
+ http: { method: 'POST', path: '/api/v1/conditions/lookup' },
99
+ roles: BOTH_ROLES,
100
+ annotations: READ_ANN,
101
+ inputSchema: { ...farmacia, codes: z.array(z.string().min(1).max(20)).min(1).max(1000).describe('Codigos nacionales.') },
102
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'POST', path: '/v1/conditions/lookup', body: { codes: args.codes } }),
103
+ },
104
+ {
105
+ name: 'createQuote',
106
+ title: 'Valorar una cesta (sin pedir)',
107
+ description: 'Valora una cesta SIN crear nada: precio por linea, ofertas desbloqueadas y las que faltan por alcanzar, envios, impuestos y pedido minimo. ' +
108
+ 'Hazlo SIEMPRE antes de un pedido y ENSEÑA el resumen al usuario (total, lineas no servibles, si llega al minimo). Se puede repetir las veces que haga falta.' +
109
+ aviso,
110
+ op: 'READ',
111
+ http: { method: 'POST', path: '/api/v1/quote' },
112
+ roles: BOTH_ROLES,
113
+ annotations: READ_ANN,
114
+ inputSchema: { ...farmacia, items: ITEMS },
115
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'POST', path: '/v1/quote', body: { items: args.items }, timeoutMs: LONG_TIMEOUT_MS }),
116
+ },
117
+ {
118
+ name: 'createOrder',
119
+ title: 'Crear un pedido REAL',
120
+ description: 'Crea un pedido REAL y firme (no se puede cancelar ni modificar por API; solo Pharmadate puede cancelarlo). ' +
121
+ 'Hazlo SOLO tras createQuote y tras la confirmacion EXPLICITA del usuario con el resumen delante. ' +
122
+ '`external_ref` es OBLIGATORIA y actua como clave de idempotencia: inventala UNA SOLA VEZ para este pedido (p. ej. "mcp-2026-11-04-1") y REUTILIZA la misma si reintentas; misma ref y misma cesta devuelve el pedido existente (200), con otra cesta da external_ref_conflict. ' +
123
+ 'Si se corta la llamada, NO repitas con otra ref: reintenta con la misma o mira listOrders con external_ref. Limite: 10 pedidos por hora.' +
124
+ aviso,
125
+ op: 'WRITE',
126
+ http: { method: 'POST', path: '/api/v1/orders' },
127
+ roles: BOTH_ROLES,
128
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
129
+ inputSchema: {
130
+ ...farmacia,
131
+ external_ref: z.string().min(1).max(255).regex(/^[^\u0000-\u001f\u007f]+$/, 'Sin caracteres de control').describe('Referencia unica del pedido (clave de idempotencia). Obligatoria.'),
132
+ items: ITEMS,
133
+ cooperativa: z.enum(['BIDAFARMA', 'HEFAME', 'COFARES', 'ALLIANCE', 'OTROS', 'PHARMADATE']).optional().describe('Cooperativa, si hay que fijarla.'),
134
+ },
135
+ handler: async (args, ctx) => {
136
+ const body = { items: args.items, external_ref: args.external_ref };
137
+ if (args.cooperativa)
138
+ body['cooperativa'] = args.cooperativa;
139
+ return operate(ctx, args.pharmacy, { method: 'POST', path: '/v1/orders', body, timeoutMs: LONG_TIMEOUT_MS }, { externalRef: args.external_ref });
140
+ },
141
+ },
142
+ {
143
+ name: 'listOrders',
144
+ title: 'Listar pedidos',
145
+ description: `Lista los pedidos de la farmacia, paginados, con filtros por fecha, estado y \`external_ref\`. Con include="items" trae las lineas y los envios.${aviso}`,
146
+ op: 'READ',
147
+ http: { method: 'GET', path: '/api/v1/orders' },
148
+ roles: BOTH_ROLES,
149
+ annotations: READ_ANN,
150
+ inputSchema: {
151
+ ...farmacia,
152
+ updated_since: z.string().max(40).optional().describe('Pedidos movidos despues de esta fecha ISO 8601 (usa el `as_of` anterior).'),
153
+ from: z.string().max(40).optional().describe('Creados desde (AAAA-MM-DD).'),
154
+ to: z.string().max(40).optional().describe('Creados hasta el final de este dia (AAAA-MM-DD).'),
155
+ external_ref: z.string().max(255).optional().describe('Busca por la referencia con la que se creo el pedido.'),
156
+ status: z.enum(['confirmed', 'sent_to_lab', 'invoiced', 'cancelled']).optional(),
157
+ status_detail: z.enum(['confirmed', 'sent_to_lab', 'shipped', 'delivered', 'invoiced', 'cancelled']).optional(),
158
+ shipment_status: z.enum(['confirmed', 'sent_to_lab', 'shipped', 'delivered', 'invoiced', 'cancelled']).optional().describe('Pedidos con algun envio en ese estado.'),
159
+ modified: z.boolean().optional().describe('true: solo pedidos con faltas definitivas.'),
160
+ include: z.enum(['items', 'shipments']).optional().describe('`items` incluye lineas y envios; `shipments` solo los envios.'),
161
+ page: PAGE,
162
+ per_page: z.number().int().min(1).max(200).optional().describe('Maximo 200 (por defecto 50).'),
163
+ },
164
+ handler: async (args, ctx) => {
165
+ const { pharmacy, ...query } = args;
166
+ return operate(ctx, pharmacy, { method: 'GET', path: '/v1/orders', query });
167
+ },
168
+ },
169
+ {
170
+ name: 'getOrder',
171
+ title: 'Detalle de un pedido',
172
+ description: `Un pedido completo: lineas, totales, impuestos, envios y estado.${aviso}`,
173
+ op: 'READ',
174
+ http: { method: 'GET', path: '/api/v1/orders/{id}' },
175
+ roles: BOTH_ROLES,
176
+ annotations: READ_ANN,
177
+ inputSchema: { ...farmacia, id: ORDER_ID },
178
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'GET', path: `/v1/orders/${args.id}` }),
179
+ },
180
+ {
181
+ name: 'getOrderModification',
182
+ title: 'Faltas y modificaciones de un pedido',
183
+ description: `Lo realmente servido frente a lo pedido: faltas definitivas, lineas modificadas y totales ajustados.${aviso}`,
184
+ op: 'READ',
185
+ http: { method: 'GET', path: '/api/v1/orders/{id}/modification' },
186
+ roles: BOTH_ROLES,
187
+ annotations: READ_ANN,
188
+ inputSchema: { ...farmacia, id: ORDER_ID },
189
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'GET', path: `/v1/orders/${args.id}/modification` }),
190
+ },
191
+ {
192
+ name: 'listOrderShipments',
193
+ title: 'Envios de un pedido',
194
+ description: `Los envios en que se reparte un pedido (Pharmadate, laboratorios, cooperativas) con su estado y entrega estimada.${aviso}`,
195
+ op: 'READ',
196
+ http: { method: 'GET', path: '/api/v1/orders/{id}/shipments' },
197
+ roles: BOTH_ROLES,
198
+ annotations: READ_ANN,
199
+ inputSchema: { ...farmacia, id: ORDER_ID },
200
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'GET', path: `/v1/orders/${args.id}/shipments` }),
201
+ },
202
+ {
203
+ name: 'getOrderShipment',
204
+ title: 'Un envio de un pedido',
205
+ description: `Un envio concreto de un pedido, con sus lineas y su estado.${aviso}`,
206
+ op: 'READ',
207
+ http: { method: 'GET', path: '/api/v1/orders/{id}/shipments/{shipment_id}' },
208
+ roles: BOTH_ROLES,
209
+ annotations: READ_ANN,
210
+ inputSchema: { ...farmacia, id: ORDER_ID, shipment_id: SHIPMENT_ID },
211
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'GET', path: `/v1/orders/${args.id}/shipments/${encodeURIComponent(args.shipment_id)}` }),
212
+ },
213
+ {
214
+ name: 'getShipment',
215
+ title: 'Un envio por su identificador',
216
+ description: `Un envio por su \`shipment_id\` global, sin necesitar el id del pedido.${aviso}`,
217
+ op: 'READ',
218
+ http: { method: 'GET', path: '/api/v1/shipments/{shipment_id}' },
219
+ roles: BOTH_ROLES,
220
+ annotations: READ_ANN,
221
+ inputSchema: { ...farmacia, shipment_id: SHIPMENT_ID },
222
+ handler: async (args, ctx) => operate(ctx, args.pharmacy, { method: 'GET', path: `/v1/shipments/${encodeURIComponent(args.shipment_id)}` }),
223
+ },
224
+ ];
225
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Registro de herramientas por rol.
3
+ *
4
+ * farmacia: 12 (las comunes, sin argumento `pharmacy`)
5
+ * partner: 19 (las 12 comunes con `pharmacy` + las 7 del CRUD de farmacias)
6
+ * admin: F2 (no incluido en esta fase)
7
+ *
8
+ * @module tools
9
+ */
10
+ import type { Role } from '../principal.js';
11
+ import type { ToolDef } from './types.js';
12
+ export type { ToolDef, ToolContext } from './types.js';
13
+ export declare function toolsForRole(role: Role, opts?: {
14
+ defaultPharmacy?: boolean;
15
+ }): ToolDef[];
@@ -0,0 +1,6 @@
1
+ import { commonTools } from './common.js';
2
+ import { partnerTools } from './partner.js';
3
+ export function toolsForRole(role, opts = {}) {
4
+ const comunes = commonTools(role, opts);
5
+ return role === 'partner' ? [...comunes, ...partnerTools()] : comunes;
6
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Plantilla comun de las herramientas operativas (las que, con una key de partner, necesitan una farmacia).
3
+ *
4
+ * @module tools/operate
5
+ */
6
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
7
+ import { type ApiRequest } from '../client.js';
8
+ import { type PharmacyRefArg } from '../pharmacy-resolver.js';
9
+ import { type ErrorContext } from '../results.js';
10
+ import type { ToolContext } from './types.js';
11
+ /**
12
+ * 1) resuelve la farmacia (solo partner), 2) llama a la API con `X-Pharmacy-Ref` ya verificado, 3) traduce la respuesta.
13
+ * Con `pharmacy = null` no se resuelve nada (catalogo). El cuerpo que se pasa NUNCA lleva `pharmacy` ni `pharmacy_ref`.
14
+ */
15
+ export declare function operate(ctx: ToolContext, pharmacy: PharmacyRefArg | undefined | null, req: Omit<ApiRequest, 'pharmacyCif'>, errorCtx?: ErrorContext): Promise<CallToolResult>;
@@ -0,0 +1,24 @@
1
+ import { ApiNetworkError } from '../client.js';
2
+ import { resolvePharmacy } from '../pharmacy-resolver.js';
3
+ import { apiErrorResult, networkErrorResult, okResult } from '../results.js';
4
+ /**
5
+ * 1) resuelve la farmacia (solo partner), 2) llama a la API con `X-Pharmacy-Ref` ya verificado, 3) traduce la respuesta.
6
+ * Con `pharmacy = null` no se resuelve nada (catalogo). El cuerpo que se pasa NUNCA lleva `pharmacy` ni `pharmacy_ref`.
7
+ */
8
+ export async function operate(ctx, pharmacy, req, errorCtx = {}) {
9
+ try {
10
+ // `null`: la operacion no necesita farmacia (catalogo): no se resuelve nada ni se envia X-Pharmacy-Ref
11
+ const resolution = pharmacy === null ? { ok: true, cif: undefined } : await resolvePharmacy(ctx.client, ctx.principal, pharmacy ?? ctx.defaultPharmacy);
12
+ if (!resolution.ok)
13
+ return resolution.result;
14
+ const res = await ctx.client.request({ ...req, ...(resolution.cif ? { pharmacyCif: resolution.cif } : {}) });
15
+ if (!res.ok)
16
+ return apiErrorResult(res, { pharmacyId: 'pharmacy' in resolution ? resolution.pharmacy?.id : undefined, ...errorCtx });
17
+ return okResult(res.body, { request_id: res.requestId, http_status: res.status });
18
+ }
19
+ catch (err) {
20
+ if (err instanceof ApiNetworkError)
21
+ return networkErrorResult(err);
22
+ throw err;
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ import { type ToolDef } from './types.js';
2
+ export declare function partnerTools(): ToolDef[];