@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
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Herramientas SOLO de partner: el CRUD de farmacias propias (`/api/v1/pharmacies`).
3
+ *
4
+ * Estas rutas no usan `X-Pharmacy-Ref` ni dan de alta nada por su cuenta: solo el partner dueno las ve y las toca.
5
+ * Una API key de farmacia da 403 en ellas, asi que ni siquiera se registran para ese rol.
6
+ *
7
+ * @module tools/partner
8
+ */
9
+ import { z } from 'zod';
10
+ import { ApiNetworkError } from '../client.js';
11
+ import { apiErrorResult, applyKeyPolicy, errResult, networkErrorResult, okResult } from '../results.js';
12
+ import { PARTNER_ONLY, READ_ANN } from './types.js';
13
+ const REVEAL = z
14
+ .boolean()
15
+ .optional()
16
+ .describe('Si es true, la API key COMPLETA se entrega al usuario en un bloque aparte (audience: user). Por defecto (false) solo se ve el prefijo. ' +
17
+ 'OJO: la key puede quedar en el historial del chat. Esta key solo se muestra esta vez.');
18
+ const FARMACIA_ID = z.number().int().positive().describe('`id` de la farmacia en Pharmadate (el que devuelven listPharmacies / createPharmacy).');
19
+ const COOPERATIVAS = z.array(z.enum(['BIDAFARMA', 'HEFAME', 'COFARES', 'ALLIANCE', 'OTROS', 'PHARMADATE']));
20
+ const BILLING = z
21
+ .object({
22
+ direccion: z.string().min(1).max(255),
23
+ codigo_postal: z.string().min(1).max(255),
24
+ localidad: z.string().min(1).max(255),
25
+ ciudad: z.string().min(1).max(255).describe('Provincia.'),
26
+ })
27
+ .nullable()
28
+ .describe('Direccion de FACTURACION si es distinta de la de entrega (los cuatro campos son obligatorios). null la quita (solo en updatePharmacy).');
29
+ const txt = (max = 255) => z.string().min(1).max(max);
30
+ /** Llamada del CRUD (sin farmacia que resolver) con traduccion de errores. */
31
+ async function crud(ctx, req, errorCtx, onOk) {
32
+ try {
33
+ const res = await ctx.client.request(req);
34
+ if (!res.ok)
35
+ return apiErrorResult(res, errorCtx);
36
+ if (onOk)
37
+ return onOk(res.body, res.requestId, res.status);
38
+ return okResult(res.body, { request_id: res.requestId, http_status: res.status });
39
+ }
40
+ catch (err) {
41
+ if (err instanceof ApiNetworkError)
42
+ return networkErrorResult(err);
43
+ throw err;
44
+ }
45
+ }
46
+ export function partnerTools() {
47
+ return [
48
+ {
49
+ name: 'listPharmacies',
50
+ title: 'Listar mis farmacias',
51
+ description: 'Lista SOLO las farmacias de este partner (las de otros no existen para ti), con filtros por `cif`, `partner_pharmacy_id`, `active` y `updated_since`. ' +
52
+ 'Incluye las dadas de baja (`active: false`). Usala para COMPROBAR un CIF o localizar el `id` de una farmacia antes de operar. Nunca devuelve la API key completa, solo su prefijo.',
53
+ op: 'READ',
54
+ http: { method: 'GET', path: '/api/v1/pharmacies' },
55
+ roles: PARTNER_ONLY,
56
+ annotations: READ_ANN,
57
+ inputSchema: {
58
+ page: z.number().int().min(1).optional(),
59
+ per_page: z.number().int().min(1).max(200).optional().describe('Maximo 200 (por defecto 50).'),
60
+ cif: z.string().max(20).optional().describe('CIF exacto.'),
61
+ partner_pharmacy_id: z.string().max(255).optional().describe('Tu identificador de la farmacia.'),
62
+ active: z.boolean().optional().describe('true solo activas, false solo dadas de baja.'),
63
+ updated_since: z.string().max(40).optional().describe('ISO 8601 (el `as_of` de una pasada anterior).'),
64
+ },
65
+ handler: async (args, ctx) => crud(ctx, { method: 'GET', path: '/v1/pharmacies', query: args }, {}),
66
+ },
67
+ {
68
+ name: 'getPharmacy',
69
+ title: 'Detalle de una farmacia',
70
+ description: 'Una farmacia propia por su `id`: datos, direccion de facturacion, estado (`active`) y prefijo de su API key. Una farmacia ajena responde como si no existiera.',
71
+ op: 'READ',
72
+ http: { method: 'GET', path: '/api/v1/pharmacies/{id}' },
73
+ roles: PARTNER_ONLY,
74
+ annotations: READ_ANN,
75
+ inputSchema: { id: FARMACIA_ID },
76
+ handler: async (args, ctx) => crud(ctx, { method: 'GET', path: `/v1/pharmacies/${args.id}` }, { pharmacyId: args.id }),
77
+ },
78
+ {
79
+ name: 'createPharmacy',
80
+ title: 'Dar de alta una farmacia',
81
+ description: 'Da de alta una farmacia nueva para este partner. El CIF es unico en TODA la plataforma y no se puede cambiar despues; el email tambien es unico. ' +
82
+ 'Comprueba antes con listPharmacies que no existe ya. Devuelve la farmacia con su API key (por defecto solo el prefijo; con reveal_api_key=true la key completa va al usuario, que debe guardarla). ' +
83
+ 'Confirma los datos con el usuario antes de crearla.',
84
+ op: 'WRITE',
85
+ http: { method: 'POST', path: '/api/v1/pharmacies' },
86
+ roles: PARTNER_ONLY,
87
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
88
+ inputSchema: {
89
+ cif: z.string().min(8).max(12).describe('CIF/NIF de la farmacia (8 a 12 letras o numeros, sin espacios).'),
90
+ nombre: txt(),
91
+ direccion: txt().describe('Direccion de ENTREGA del pedido.'),
92
+ codigo_postal: txt(),
93
+ localidad: txt(),
94
+ ciudad: txt().describe('Provincia.'),
95
+ email: z.string().email().max(255).describe('Unico: no puede usarlo otra farmacia.'),
96
+ telefono: txt().optional(),
97
+ cooperativas: COOPERATIVAS.optional().describe('Cooperativas con las que trabaja.'),
98
+ recargo_equivalencia: z.boolean().optional(),
99
+ partner_pharmacy_id: txt().optional().describe('Tu identificador de la farmacia (unico por partner).'),
100
+ billing: BILLING.optional(),
101
+ reveal_api_key: REVEAL,
102
+ },
103
+ handler: async (args, ctx) => {
104
+ const { reveal_api_key, ...body } = args;
105
+ return crud(ctx, { method: 'POST', path: '/v1/pharmacies', body }, {}, (b, rid, st) => applyKeyPolicy(b, reveal_api_key === true, { request_id: rid, http_status: st }));
106
+ },
107
+ },
108
+ {
109
+ name: 'updatePharmacy',
110
+ title: 'Modificar una farmacia',
111
+ description: 'Modifica datos de una farmacia propia y ACTIVA (solo los campos que envies). El CIF no se puede cambiar. `cooperativas` es la lista completa (vacia las quita) y `billing: null` quita la direccion de facturacion. ' +
112
+ 'Muestra al usuario que vas a cambiar antes de hacerlo. Una farmacia de baja no se puede modificar: reactivala antes.',
113
+ op: 'WRITE',
114
+ http: { method: 'PATCH', path: '/api/v1/pharmacies/{id}' },
115
+ roles: PARTNER_ONLY,
116
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
117
+ inputSchema: {
118
+ id: FARMACIA_ID,
119
+ nombre: txt().optional(),
120
+ direccion: txt().optional().describe('Direccion de ENTREGA.'),
121
+ codigo_postal: txt().optional(),
122
+ localidad: txt().optional(),
123
+ ciudad: txt().optional().describe('Provincia.'),
124
+ email: z.string().email().max(255).optional(),
125
+ telefono: txt().optional(),
126
+ cooperativas: COOPERATIVAS.optional(),
127
+ recargo_equivalencia: z.boolean().optional(),
128
+ partner_pharmacy_id: txt().optional(),
129
+ billing: BILLING.optional(),
130
+ },
131
+ handler: async (args, ctx) => {
132
+ const { id, ...cambios } = args;
133
+ if (Object.values(cambios).every((v) => v === undefined)) {
134
+ return errResult('Indica al menos un campo a modificar.', { error_code: 'invalid_request' });
135
+ }
136
+ return crud(ctx, { method: 'PATCH', path: `/v1/pharmacies/${id}`, body: cambios }, { pharmacyId: id });
137
+ },
138
+ },
139
+ {
140
+ name: 'deactivatePharmacy',
141
+ title: 'Dar de baja una farmacia',
142
+ description: 'Da de baja una farmacia propia (baja LOGICA): deja de poder operar y su API key actual se BORRA; los pedidos y albaranes se conservan y el CIF queda bloqueado. ' +
143
+ 'Se deshace con reactivatePharmacy (con una API key nueva). Es idempotente. Pide confirmacion explicita al usuario antes de ejecutarla.',
144
+ op: 'DELETE',
145
+ http: { method: 'DELETE', path: '/api/v1/pharmacies/{id}' },
146
+ roles: PARTNER_ONLY,
147
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
148
+ inputSchema: { id: FARMACIA_ID },
149
+ handler: async (args, ctx) => crud(ctx, { method: 'DELETE', path: `/v1/pharmacies/${args.id}` }, { pharmacyId: args.id }),
150
+ },
151
+ {
152
+ name: 'reactivatePharmacy',
153
+ title: 'Reactivar una farmacia de baja',
154
+ description: 'Reactiva una farmacia propia dada de baja (deshace deactivatePharmacy). Genera una API key NUEVA (la de antes se borro). ' +
155
+ 'Si ya estaba activa responde pharmacy_active sin tocar nada. Con reveal_api_key=true la key completa va al usuario; sin el, solo veras el prefijo y la key no se muestra en ningun otro sitio (tendrias que rotarla).',
156
+ op: 'WRITE',
157
+ http: { method: 'POST', path: '/api/v1/pharmacies/{id}/reactivate' },
158
+ roles: PARTNER_ONLY,
159
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
160
+ inputSchema: { id: FARMACIA_ID, reveal_api_key: REVEAL },
161
+ handler: async (args, ctx) => crud(ctx, { method: 'POST', path: `/v1/pharmacies/${args.id}/reactivate`, body: {} }, { pharmacyId: args.id }, (b, rid, st) => applyKeyPolicy(b, args.reveal_api_key === true, { request_id: rid, http_status: st })),
162
+ },
163
+ {
164
+ name: 'rotatePharmacyApiKey',
165
+ title: 'Rotar la API key de una farmacia',
166
+ description: 'Genera una API key NUEVA para una farmacia activa e INVALIDA LA ANTERIOR EN EL ACTO (cualquier integracion que la use dejara de funcionar). ' +
167
+ 'Sirve para recuperar una key perdida. Pide confirmacion explicita al usuario. Usa reveal_api_key=true para que la key nueva llegue al usuario: sin ese flag la key nueva NO se muestra en ningun sitio y la anterior ya no vale.',
168
+ op: 'DELETE',
169
+ http: { method: 'POST', path: '/api/v1/pharmacies/{id}/api-key' },
170
+ roles: PARTNER_ONLY,
171
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
172
+ inputSchema: { id: FARMACIA_ID, reveal_api_key: REVEAL },
173
+ handler: async (args, ctx) => crud(ctx, { method: 'POST', path: `/v1/pharmacies/${args.id}/api-key`, body: {} }, { pharmacyId: args.id }, (b, rid, st) => applyKeyPolicy(b, args.reveal_api_key === true, { request_id: rid, http_status: st })),
174
+ },
175
+ ];
176
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tipos compartidos de las herramientas.
3
+ *
4
+ * Cada herramienta tiene el nombre de su `operationId` en frontend/api-docs/openapi.yaml, y declara su metodo y ruta
5
+ * (`http`) para que el test de contrato compruebe que coinciden con el OpenAPI.
6
+ *
7
+ * @module tools/types
8
+ */
9
+ import type { ZodRawShape } from 'zod';
10
+ import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
11
+ import type { PharmadateClient } from '../client.js';
12
+ import type { Principal, Role } from '../principal.js';
13
+ import type { PharmacyRefArg } from '../pharmacy-resolver.js';
14
+ import type { OperationType } from '../operation-mode.js';
15
+ export interface ToolContext {
16
+ client: PharmadateClient;
17
+ principal: Principal;
18
+ /** Farmacia por defecto (stdio con PHARMADATE_PHARMACY_REF): se usa si la herramienta no recibe `pharmacy`. */
19
+ defaultPharmacy?: PharmacyRefArg | undefined;
20
+ }
21
+ export interface ToolDef {
22
+ /** = operationId del OpenAPI. */
23
+ name: string;
24
+ title: string;
25
+ description: string;
26
+ op: OperationType;
27
+ /** Metodo y ruta del OpenAPI (con `{id}`): lo verifica el test de contrato. */
28
+ http: {
29
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
30
+ path: string;
31
+ };
32
+ roles: readonly Role[];
33
+ annotations: ToolAnnotations;
34
+ inputSchema: ZodRawShape;
35
+ handler: (args: any, ctx: ToolContext) => Promise<CallToolResult>;
36
+ }
37
+ export declare const BOTH_ROLES: readonly Role[];
38
+ export declare const PARTNER_ONLY: readonly Role[];
39
+ export declare const READ_ANN: ToolAnnotations;
@@ -0,0 +1,3 @@
1
+ export const BOTH_ROLES = ['partner', 'pharmacy'];
2
+ export const PARTNER_ONLY = ['partner'];
3
+ export const READ_ANN = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Version del paquete, leida de package.json (esta un nivel por encima de `src/` y de `dist/`).
3
+ * @module version
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ export const VERSION = (() => {
8
+ try {
9
+ const ruta = fileURLToPath(new URL('../package.json', import.meta.url));
10
+ return JSON.parse(readFileSync(ruta, 'utf-8')).version;
11
+ }
12
+ catch {
13
+ return '0.0.0';
14
+ }
15
+ })();
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@pharmadate/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "mcpName": "es.pharmadatepedido/mcp",
4
+ "version": "0.1.1",
5
+ "description": "Servidor MCP de Pharmadate: consulta el catalogo, valora y cursa pedidos y gestiona las farmacias de un partner desde Claude, ChatGPT, Cursor o cualquier cliente MCP. 19 herramientas sobre la API v1, stdio, Streamable HTTP y OAuth 2.1.",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "pharmadate-mcp": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist/",
14
+ "README.md",
15
+ "LICENSE",
16
+ "server.json"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20.0.0"
20
+ },
21
+ "author": {
22
+ "name": "Nubiia",
23
+ "email": "samuel.fraga@nubiia.es",
24
+ "url": "https://nubiia.es"
25
+ },
26
+ "license": "MIT",
27
+ "homepage": "https://www.pharmadatepedido.es/api-docs/mcp.html",
28
+ "bugs": {
29
+ "email": "samuel.fraga@nubiia.es"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Rubenga11/pedido-directo.git",
34
+ "directory": "mcp"
35
+ },
36
+ "keywords": [
37
+ "mcp",
38
+ "model-context-protocol",
39
+ "pharmadate",
40
+ "pharmacy",
41
+ "farmacia",
42
+ "topfarma",
43
+ "claude",
44
+ "chatgpt"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "registry": "https://registry.npmjs.org/"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "start": "node dist/index.js",
53
+ "dev": "tsx src/index.ts",
54
+ "test": "vitest run --exclude '**/integration/**'",
55
+ "test:integration": "vitest run integration/",
56
+ "test:all": "vitest run",
57
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
58
+ "prepublishOnly": "npm run build && npm test"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.32.2",
62
+ "express": "^4.21.0",
63
+ "pino": "^10.3.1",
64
+ "zod": "^3.25.0"
65
+ },
66
+ "devDependencies": {
67
+ "@types/express": "^4.17.21",
68
+ "@types/js-yaml": "^4.0.9",
69
+ "@types/node": "^20.10.0",
70
+ "js-yaml": "^4.1.0",
71
+ "tsx": "^4.6.0",
72
+ "typescript": "^5.3.0",
73
+ "undici": "^7.0.0",
74
+ "vitest": "^2.1.8",
75
+ "zod-to-json-schema": "^3.25.2"
76
+ }
77
+ }
package/server.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "es.pharmadatepedido/mcp",
4
+ "title": "Pharmadate",
5
+ "description": "Pharmadate: catalogo, valoracion y pedidos de farmacia, y gestion de farmacias para partners.",
6
+ "version": "0.1.1",
7
+ "websiteUrl": "https://www.pharmadatepedido.es/api-docs/mcp.html",
8
+ "repository": {
9
+ "url": "https://github.com/Rubenga11/pedido-directo",
10
+ "source": "github",
11
+ "subfolder": "mcp"
12
+ },
13
+ "packages": [
14
+ {
15
+ "registryType": "npm",
16
+ "registryBaseUrl": "https://registry.npmjs.org",
17
+ "identifier": "@pharmadate/mcp",
18
+ "version": "0.1.1",
19
+ "runtimeHint": "npx",
20
+ "transport": {
21
+ "type": "stdio"
22
+ },
23
+ "environmentVariables": [
24
+ {
25
+ "name": "PHARMADATE_API_KEY",
26
+ "description": "API key de Pharmadate: de partner (pk_partner_...) o de farmacia (pk_live_...)",
27
+ "isRequired": true,
28
+ "isSecret": true
29
+ },
30
+ {
31
+ "name": "PHARMADATE_MCP_MODE",
32
+ "description": "READ_ONLY (por defecto: solo lectura y valoracion) o STANDARD (permite pedidos y gestion de farmacias)",
33
+ "isRequired": false,
34
+ "default": "READ_ONLY",
35
+ "choices": ["READ_ONLY", "STANDARD"]
36
+ },
37
+ {
38
+ "name": "PHARMADATE_PHARMACY_REF",
39
+ "description": "Solo partners: CIF de la farmacia con la que operar por defecto",
40
+ "isRequired": false
41
+ },
42
+ {
43
+ "name": "PHARMADATE_API_URL",
44
+ "description": "URL de la API (por defecto https://www.pharmadatepedido.es/api)",
45
+ "isRequired": false
46
+ }
47
+ ]
48
+ }
49
+ ],
50
+ "remotes": [
51
+ {
52
+ "type": "streamable-http",
53
+ "url": "https://www.pharmadatepedido.es/mcp"
54
+ }
55
+ ]
56
+ }