@cyanheads/openfda-mcp-server 0.1.6
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/CLAUDE.md +248 -0
- package/Dockerfile +99 -0
- package/LICENSE +190 -0
- package/README.md +281 -0
- package/dist/config/server-config.d.ts +14 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +22 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/count.tool.d.ts +42 -0
- package/dist/mcp-server/tools/definitions/count.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/count.tool.js +106 -0
- package/dist/mcp-server/tools/definitions/count.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/get-drug-label.tool.d.ts +21 -0
- package/dist/mcp-server/tools/definitions/get-drug-label.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/get-drug-label.tool.js +102 -0
- package/dist/mcp-server/tools/definitions/get-drug-label.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/index.d.ts +68 -0
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/index.js +21 -0
- package/dist/mcp-server/tools/definitions/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/lookup-ndc.tool.d.ts +21 -0
- package/dist/mcp-server/tools/definitions/lookup-ndc.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/lookup-ndc.tool.js +101 -0
- package/dist/mcp-server/tools/definitions/lookup-ndc.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/search-adverse-events.tool.d.ts +26 -0
- package/dist/mcp-server/tools/definitions/search-adverse-events.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/search-adverse-events.tool.js +147 -0
- package/dist/mcp-server/tools/definitions/search-adverse-events.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/search-device-clearances.tool.d.ts +25 -0
- package/dist/mcp-server/tools/definitions/search-device-clearances.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/search-device-clearances.tool.js +108 -0
- package/dist/mcp-server/tools/definitions/search-device-clearances.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/search-drug-approvals.tool.d.ts +23 -0
- package/dist/mcp-server/tools/definitions/search-drug-approvals.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/search-drug-approvals.tool.js +136 -0
- package/dist/mcp-server/tools/definitions/search-drug-approvals.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/search-recalls.tool.d.ts +30 -0
- package/dist/mcp-server/tools/definitions/search-recalls.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/search-recalls.tool.js +105 -0
- package/dist/mcp-server/tools/definitions/search-recalls.tool.js.map +1 -0
- package/dist/services/openfda/openfda-service.d.ts +25 -0
- package/dist/services/openfda/openfda-service.d.ts.map +1 -0
- package/dist/services/openfda/openfda-service.js +111 -0
- package/dist/services/openfda/openfda-service.js.map +1 -0
- package/dist/services/openfda/types.d.ts +30 -0
- package/dist/services/openfda/types.d.ts.map +1 -0
- package/dist/services/openfda/types.js +6 -0
- package/dist/services/openfda/types.js.map +1 -0
- package/package.json +88 -0
- package/server.json +98 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for searching FDA drug application approvals (NDAs and ANDAs)
|
|
3
|
+
* via the Drugs@FDA endpoint.
|
|
4
|
+
* @module mcp-server/tools/definitions/search-drug-approvals
|
|
5
|
+
*/
|
|
6
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import { getOpenFdaService } from '../../../services/openfda/openfda-service.js';
|
|
8
|
+
/** Exported tool definition for searching drug approvals. */
|
|
9
|
+
export const searchDrugApprovalsTool = tool('openfda_search_drug_approvals', {
|
|
10
|
+
description: 'Search the Drugs@FDA database for drug application approvals (NDAs and ANDAs). Returns application details, sponsor info, and full submission history.',
|
|
11
|
+
annotations: { readOnlyHint: true },
|
|
12
|
+
input: z.object({
|
|
13
|
+
search: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('openFDA search query. Examples: openfda.brand_name:"humira", sponsor_name:"pfizer", submissions.submission_type:"ORIG" AND submissions.review_priority:"PRIORITY". Omit to browse recent.'),
|
|
17
|
+
sort: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Sort field and order. Example: submissions.submission_status_date:desc'),
|
|
21
|
+
limit: z
|
|
22
|
+
.number()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(1000)
|
|
25
|
+
.default(10)
|
|
26
|
+
.describe('Maximum number of records to return (1-1000, default 10)'),
|
|
27
|
+
skip: z
|
|
28
|
+
.number()
|
|
29
|
+
.min(0)
|
|
30
|
+
.max(25000)
|
|
31
|
+
.default(0)
|
|
32
|
+
.describe('Number of records to skip for pagination (0-25000, default 0)'),
|
|
33
|
+
}),
|
|
34
|
+
output: z.object({
|
|
35
|
+
meta: z
|
|
36
|
+
.object({
|
|
37
|
+
total: z.number().describe('Total matching records'),
|
|
38
|
+
skip: z.number().describe('Pagination offset'),
|
|
39
|
+
limit: z.number().describe('Records returned'),
|
|
40
|
+
lastUpdated: z.string().describe('Dataset last updated date'),
|
|
41
|
+
})
|
|
42
|
+
.describe('Response metadata'),
|
|
43
|
+
results: z
|
|
44
|
+
.array(z.record(z.string(), z.any()))
|
|
45
|
+
.describe('Drug application records with submission history'),
|
|
46
|
+
message: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Guidance when results are empty or search can be refined'),
|
|
50
|
+
}),
|
|
51
|
+
async handler(input, ctx) {
|
|
52
|
+
const service = getOpenFdaService();
|
|
53
|
+
const response = await service.query('drug/drugsfda', {
|
|
54
|
+
search: input.search,
|
|
55
|
+
sort: input.sort,
|
|
56
|
+
limit: input.limit,
|
|
57
|
+
skip: input.skip,
|
|
58
|
+
}, ctx);
|
|
59
|
+
ctx.log.info('Drug approval search completed', {
|
|
60
|
+
search: input.search,
|
|
61
|
+
total: response.meta.total,
|
|
62
|
+
returned: response.results.length,
|
|
63
|
+
});
|
|
64
|
+
const message = response.results.length === 0
|
|
65
|
+
? 'No drug approvals matched the query. Try broader terms, check field names (e.g. openfda.brand_name, sponsor_name), or remove filters.'
|
|
66
|
+
: undefined;
|
|
67
|
+
return {
|
|
68
|
+
meta: response.meta,
|
|
69
|
+
results: response.results,
|
|
70
|
+
message,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
format: (result) => {
|
|
74
|
+
if (result.results.length === 0) {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: result.message ?? 'No drug approvals found.',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
const lines = [
|
|
83
|
+
`**${result.meta.total.toLocaleString()} total results** (showing ${result.results.length}, skip: ${result.meta.skip}) | Data updated: ${result.meta.lastUpdated}\n`,
|
|
84
|
+
];
|
|
85
|
+
for (const r of result.results) {
|
|
86
|
+
const openfda = r.openfda ?? {};
|
|
87
|
+
const brandName = (openfda.brand_name ?? [])[0] ?? '';
|
|
88
|
+
const genericName = (openfda.generic_name ?? [])[0] ?? '';
|
|
89
|
+
const title = brandName || genericName || r.application_number || 'Unknown';
|
|
90
|
+
lines.push(`### ${title}`);
|
|
91
|
+
lines.push(`**Application:** ${r.application_number ?? 'N/A'} | **Sponsor:** ${r.sponsor_name ?? 'N/A'}`);
|
|
92
|
+
if (brandName && genericName)
|
|
93
|
+
lines.push(`**Brand:** ${brandName} | **Generic:** ${genericName}`);
|
|
94
|
+
if (openfda.route)
|
|
95
|
+
lines.push(`**Route:** ${openfda.route.join(', ')}`);
|
|
96
|
+
if (openfda.product_type)
|
|
97
|
+
lines.push(`**Type:** ${openfda.product_type.join(', ')}`);
|
|
98
|
+
const products = r.products ?? [];
|
|
99
|
+
if (products.length > 0) {
|
|
100
|
+
lines.push('**Products:**');
|
|
101
|
+
for (const p of products) {
|
|
102
|
+
const ingredients = (p.active_ingredients ?? [])
|
|
103
|
+
.map((i) => `${i.name} ${i.strength ?? ''}`.trim())
|
|
104
|
+
.join(', ');
|
|
105
|
+
const parts = [
|
|
106
|
+
p.brand_name,
|
|
107
|
+
ingredients ? `(${ingredients})` : null,
|
|
108
|
+
p.dosage_form,
|
|
109
|
+
p.route,
|
|
110
|
+
p.marketing_status,
|
|
111
|
+
].filter(Boolean);
|
|
112
|
+
lines.push(`- ${parts.join(' | ')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const submissions = r.submissions ?? [];
|
|
116
|
+
if (submissions.length > 0) {
|
|
117
|
+
lines.push('**Submissions:**');
|
|
118
|
+
for (const s of submissions.slice(0, 10)) {
|
|
119
|
+
const parts = [
|
|
120
|
+
s.submission_type,
|
|
121
|
+
s.submission_number ? `#${s.submission_number}` : null,
|
|
122
|
+
s.submission_status,
|
|
123
|
+
s.submission_status_date,
|
|
124
|
+
s.review_priority ? `(${s.review_priority})` : null,
|
|
125
|
+
].filter(Boolean);
|
|
126
|
+
lines.push(`- ${parts.join(' | ')}`);
|
|
127
|
+
}
|
|
128
|
+
if (submissions.length > 10)
|
|
129
|
+
lines.push(`- ... and ${submissions.length - 10} more`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
}
|
|
133
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=search-drug-approvals.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-drug-approvals.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-drug-approvals.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for searching openFDA enforcement reports and recall actions.
|
|
3
|
+
* @module mcp-server/tools/definitions/search-recalls
|
|
4
|
+
*/
|
|
5
|
+
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
export declare const searchRecallsTool: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
7
|
+
category: z.ZodEnum<{
|
|
8
|
+
drug: "drug";
|
|
9
|
+
food: "food";
|
|
10
|
+
device: "device";
|
|
11
|
+
}>;
|
|
12
|
+
endpoint: z.ZodDefault<z.ZodEnum<{
|
|
13
|
+
enforcement: "enforcement";
|
|
14
|
+
recall: "recall";
|
|
15
|
+
}>>;
|
|
16
|
+
search: z.ZodOptional<z.ZodString>;
|
|
17
|
+
sort: z.ZodOptional<z.ZodString>;
|
|
18
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
skip: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
21
|
+
meta: z.ZodObject<{
|
|
22
|
+
total: z.ZodNumber;
|
|
23
|
+
skip: z.ZodNumber;
|
|
24
|
+
limit: z.ZodNumber;
|
|
25
|
+
lastUpdated: z.ZodString;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
results: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
28
|
+
message: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
//# sourceMappingURL=search-recalls.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-recalls.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-recalls.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAiBjD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;kBA0G5B,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tool for searching openFDA enforcement reports and recall actions.
|
|
3
|
+
* @module mcp-server/tools/definitions/search-recalls
|
|
4
|
+
*/
|
|
5
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { getOpenFdaService } from '../../../services/openfda/openfda-service.js';
|
|
8
|
+
const Category = z.enum(['drug', 'food', 'device']).describe('Product category');
|
|
9
|
+
const Endpoint = z
|
|
10
|
+
.enum(['enforcement', 'recall'])
|
|
11
|
+
.default('enforcement')
|
|
12
|
+
.describe('Report type. Default enforcement. The recall endpoint is only available for devices.');
|
|
13
|
+
/** Truncate a string to `max` characters, appending ellipsis when trimmed. */
|
|
14
|
+
function truncate(value, max) {
|
|
15
|
+
if (!value)
|
|
16
|
+
return 'N/A';
|
|
17
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
18
|
+
}
|
|
19
|
+
export const searchRecallsTool = tool('openfda_search_recalls', {
|
|
20
|
+
description: 'Search enforcement reports and recall actions across drugs, food, and devices.',
|
|
21
|
+
annotations: { readOnlyHint: true },
|
|
22
|
+
input: z.object({
|
|
23
|
+
category: Category,
|
|
24
|
+
endpoint: Endpoint,
|
|
25
|
+
search: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('openFDA search query. Examples: classification:"Class I", recalling_firm:"pfizer", reason_for_recall:"undeclared allergen".'),
|
|
29
|
+
sort: z.string().optional().describe('Sort expression. Example: report_date:desc.'),
|
|
30
|
+
limit: z
|
|
31
|
+
.number()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(1000)
|
|
34
|
+
.default(10)
|
|
35
|
+
.describe('Maximum number of records to return (1-1000).'),
|
|
36
|
+
skip: z.number().min(0).max(25000).default(0).describe('Pagination offset (0-25000).'),
|
|
37
|
+
}),
|
|
38
|
+
output: z.object({
|
|
39
|
+
meta: z
|
|
40
|
+
.object({
|
|
41
|
+
total: z.number().describe('Total matching records'),
|
|
42
|
+
skip: z.number().describe('Pagination offset'),
|
|
43
|
+
limit: z.number().describe('Records returned'),
|
|
44
|
+
lastUpdated: z.string().describe('Dataset last updated date'),
|
|
45
|
+
})
|
|
46
|
+
.describe('Response metadata'),
|
|
47
|
+
results: z.array(z.record(z.string(), z.any())).describe('Enforcement/recall records'),
|
|
48
|
+
message: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Guidance when results are empty or search can be refined'),
|
|
52
|
+
}),
|
|
53
|
+
async handler(input, ctx) {
|
|
54
|
+
const endpointValue = input.endpoint ?? 'enforcement';
|
|
55
|
+
if (endpointValue === 'recall' && input.category !== 'device') {
|
|
56
|
+
throw validationError('The recall endpoint is only available for devices. Use enforcement for drug and food recalls.');
|
|
57
|
+
}
|
|
58
|
+
const service = getOpenFdaService();
|
|
59
|
+
const response = await service.query(`${input.category}/${endpointValue}`, {
|
|
60
|
+
search: input.search,
|
|
61
|
+
sort: input.sort,
|
|
62
|
+
limit: input.limit,
|
|
63
|
+
skip: input.skip,
|
|
64
|
+
}, ctx);
|
|
65
|
+
ctx.log.info('Recall search completed', {
|
|
66
|
+
category: input.category,
|
|
67
|
+
endpoint: endpointValue,
|
|
68
|
+
total: response.meta.total,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
meta: response.meta,
|
|
72
|
+
results: response.results,
|
|
73
|
+
message: response.results.length === 0
|
|
74
|
+
? `No recall/enforcement records matched${input.search ? ` search: ${input.search}` : ''} in ${input.category}/${endpointValue}. Try broadening filters or check field names (e.g. classification, recalling_firm, reason_for_recall).`
|
|
75
|
+
: undefined,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
format: (result) => {
|
|
79
|
+
if (result.results.length === 0) {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
type: 'text',
|
|
83
|
+
text: result.message ?? 'No results found.',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
const header = `**${result.meta.total.toLocaleString()} total records** (showing ${result.results.length}, offset ${result.meta.skip}) | Last updated: ${result.meta.lastUpdated}\n`;
|
|
88
|
+
const records = result.results.map((r) => {
|
|
89
|
+
const lines = [
|
|
90
|
+
`**Recall #${r.recall_number ?? 'N/A'}** — ${r.classification ?? 'Unclassified'}`,
|
|
91
|
+
`Firm: ${r.recalling_firm ?? 'N/A'}`,
|
|
92
|
+
`Product: ${truncate(r.product_description, 300)}`,
|
|
93
|
+
`Reason: ${truncate(r.reason_for_recall, 300)}`,
|
|
94
|
+
`Status: ${r.status ?? 'N/A'} | ${r.voluntary_mandated ?? 'N/A'}`,
|
|
95
|
+
];
|
|
96
|
+
if (r.distribution_pattern) {
|
|
97
|
+
lines.push(`Distribution: ${r.distribution_pattern}`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
});
|
|
101
|
+
const body = records.join('\n\n---\n\n');
|
|
102
|
+
return [{ type: 'text', text: `${header}\n${body}` }];
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=search-recalls.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-recalls.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/search-recalls.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generic openFDA API client with retry, rate-limit awareness, and error normalization.
|
|
3
|
+
* @module services/openfda/openfda-service
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { type ServerConfig } from '../../config/server-config.js';
|
|
7
|
+
import type { OpenFdaQueryParams, OpenFdaResponse } from './types.js';
|
|
8
|
+
export declare class OpenFdaService {
|
|
9
|
+
private readonly baseUrl;
|
|
10
|
+
private readonly apiKey;
|
|
11
|
+
constructor(config: ServerConfig);
|
|
12
|
+
/**
|
|
13
|
+
* Execute a query against any openFDA endpoint.
|
|
14
|
+
*
|
|
15
|
+
* Handles retry with exponential backoff for transient errors (429, 5xx).
|
|
16
|
+
* Returns an empty result set for 404 (valid query, zero matches).
|
|
17
|
+
*/
|
|
18
|
+
query<T = Record<string, unknown>>(endpoint: string, params: OpenFdaQueryParams, ctx: Context): Promise<OpenFdaResponse<T>>;
|
|
19
|
+
private buildUrl;
|
|
20
|
+
private normalizeResponse;
|
|
21
|
+
private handleErrorResponse;
|
|
22
|
+
}
|
|
23
|
+
export declare function initOpenFdaService(): void;
|
|
24
|
+
export declare function getOpenFdaService(): OpenFdaService;
|
|
25
|
+
//# sourceMappingURL=openfda-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openfda-service.d.ts","sourceRoot":"","sources":["../../../src/services/openfda/openfda-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAGtD,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAItE,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;gBAEhC,MAAM,EAAE,YAAY;IAKhC;;;;;OAKG;IACG,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,EAC1B,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IA2B9B,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,iBAAiB;YAcX,mBAAmB;CA6ClC;AAMD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generic openFDA API client with retry, rate-limit awareness, and error normalization.
|
|
3
|
+
* @module services/openfda/openfda-service
|
|
4
|
+
*/
|
|
5
|
+
import { rateLimited, serviceUnavailable, validationError } from '@cyanheads/mcp-ts-core/errors';
|
|
6
|
+
import { withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
7
|
+
import { getServerConfig } from '../../config/server-config.js';
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
9
|
+
export class OpenFdaService {
|
|
10
|
+
baseUrl;
|
|
11
|
+
apiKey;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.baseUrl = config.baseUrl;
|
|
14
|
+
this.apiKey = config.apiKey;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Execute a query against any openFDA endpoint.
|
|
18
|
+
*
|
|
19
|
+
* Handles retry with exponential backoff for transient errors (429, 5xx).
|
|
20
|
+
* Returns an empty result set for 404 (valid query, zero matches).
|
|
21
|
+
*/
|
|
22
|
+
async query(endpoint, params, ctx) {
|
|
23
|
+
return await withRetry(async () => {
|
|
24
|
+
const url = this.buildUrl(endpoint, params);
|
|
25
|
+
ctx.log.debug('Querying openFDA', { endpoint, params });
|
|
26
|
+
const response = await fetch(url.toString(), {
|
|
27
|
+
signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]),
|
|
28
|
+
headers: { Accept: 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
if (response.ok) {
|
|
31
|
+
const data = (await response.json());
|
|
32
|
+
return this.normalizeResponse(data);
|
|
33
|
+
}
|
|
34
|
+
return this.handleErrorResponse(response, endpoint);
|
|
35
|
+
}, {
|
|
36
|
+
operation: `openFDA:${endpoint}`,
|
|
37
|
+
context: { requestId: ctx.requestId, timestamp: ctx.timestamp },
|
|
38
|
+
baseDelayMs: 1_000,
|
|
39
|
+
signal: ctx.signal,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
buildUrl(endpoint, params) {
|
|
43
|
+
const url = new URL(`/${endpoint}.json`, this.baseUrl);
|
|
44
|
+
if (params.search)
|
|
45
|
+
url.searchParams.set('search', params.search);
|
|
46
|
+
if (params.count)
|
|
47
|
+
url.searchParams.set('count', params.count);
|
|
48
|
+
if (params.sort)
|
|
49
|
+
url.searchParams.set('sort', params.sort);
|
|
50
|
+
if (params.limit !== undefined)
|
|
51
|
+
url.searchParams.set('limit', String(params.limit));
|
|
52
|
+
if (params.skip !== undefined)
|
|
53
|
+
url.searchParams.set('skip', String(params.skip));
|
|
54
|
+
if (this.apiKey)
|
|
55
|
+
url.searchParams.set('api_key', this.apiKey);
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
normalizeResponse(data) {
|
|
59
|
+
const meta = data.meta;
|
|
60
|
+
const pagination = meta?.results;
|
|
61
|
+
return {
|
|
62
|
+
meta: {
|
|
63
|
+
total: pagination?.total ?? 0,
|
|
64
|
+
skip: pagination?.skip ?? 0,
|
|
65
|
+
limit: pagination?.limit ?? 0,
|
|
66
|
+
lastUpdated: meta?.last_updated ?? 'unknown',
|
|
67
|
+
},
|
|
68
|
+
results: data.results ?? [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async handleErrorResponse(response, endpoint) {
|
|
72
|
+
const body = (await response.json().catch(() => null));
|
|
73
|
+
const errorObj = body?.error;
|
|
74
|
+
const errorMessage = errorObj?.message ?? `HTTP ${response.status}`;
|
|
75
|
+
if (response.status === 404) {
|
|
76
|
+
return {
|
|
77
|
+
meta: { total: 0, skip: 0, limit: 0, lastUpdated: 'unknown' },
|
|
78
|
+
results: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (response.status === 429) {
|
|
82
|
+
throw rateLimited(this.apiKey
|
|
83
|
+
? 'openFDA rate limit exceeded (240 req/min or 120K/day with key). Retry after a brief wait.'
|
|
84
|
+
: 'openFDA rate limit exceeded (240 req/min or 1K/day without key). Configure OPENFDA_API_KEY to increase to 120K/day.');
|
|
85
|
+
}
|
|
86
|
+
if (response.status >= 500) {
|
|
87
|
+
throw serviceUnavailable(`openFDA upstream error: ${errorMessage}`, {
|
|
88
|
+
endpoint,
|
|
89
|
+
status: response.status,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (response.status === 400) {
|
|
93
|
+
if (/25000/i.test(errorMessage)) {
|
|
94
|
+
throw validationError('Pagination limit reached: skip cannot exceed 25000. Narrow the search query with additional filters or date ranges instead of increasing skip.', { endpoint });
|
|
95
|
+
}
|
|
96
|
+
throw validationError(`openFDA query error: ${errorMessage}. Check field names and query syntax — use AND/OR for boolean operators, quotes for exact match.`, { endpoint });
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`openFDA returned HTTP ${response.status}: ${errorMessage}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/* --- Init / accessor pattern --- */
|
|
102
|
+
let _service;
|
|
103
|
+
export function initOpenFdaService() {
|
|
104
|
+
_service = new OpenFdaService(getServerConfig());
|
|
105
|
+
}
|
|
106
|
+
export function getOpenFdaService() {
|
|
107
|
+
if (!_service)
|
|
108
|
+
throw new Error('OpenFdaService not initialized — call initOpenFdaService() in setup()');
|
|
109
|
+
return _service;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=openfda-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openfda-service.js","sourceRoot":"","sources":["../../../src/services/openfda/openfda-service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACjG,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAqB,MAAM,2BAA2B,CAAC;AAG/E,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,MAAM,OAAO,cAAc;IACR,OAAO,CAAS;IAChB,MAAM,CAAqB;IAE5C,YAAY,MAAoB;QAC9B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CACT,QAAgB,EAChB,MAA0B,EAC1B,GAAY;QAEZ,OAAO,MAAM,SAAS,CACpB,KAAK,IAAI,EAAE;YACT,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC5C,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,kBAAkB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YAExD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE;gBAC3C,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;gBAC9E,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;aACxC,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA4B,CAAC;gBAChE,OAAO,IAAI,CAAC,iBAAiB,CAAI,IAAI,CAAC,CAAC;YACzC,CAAC;YAED,OAAO,IAAI,CAAC,mBAAmB,CAAI,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACzD,CAAC,EACD;YACE,SAAS,EAAE,WAAW,QAAQ,EAAE;YAChC,OAAO,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE;YAC/D,WAAW,EAAE,KAAK;YAClB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CACF,CAAC;IACJ,CAAC;IAEO,QAAQ,CAAC,QAAgB,EAAE,MAA0B;QAC3D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,QAAQ,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACvD,IAAI,MAAM,CAAC,MAAM;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACjE,IAAI,MAAM,CAAC,KAAK;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9D,IAAI,MAAM,CAAC,IAAI;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3D,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpF,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QACjF,IAAI,IAAI,CAAC,MAAM;YAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9D,OAAO,GAAG,CAAC;IACb,CAAC;IAEO,iBAAiB,CAAI,IAA6B;QACxD,MAAM,IAAI,GAAG,IAAI,CAAC,IAA2C,CAAC;QAC9D,MAAM,UAAU,GAAG,IAAI,EAAE,OAA8C,CAAC;QACxE,OAAO;YACL,IAAI,EAAE;gBACJ,KAAK,EAAG,UAAU,EAAE,KAAgB,IAAI,CAAC;gBACzC,IAAI,EAAG,UAAU,EAAE,IAAe,IAAI,CAAC;gBACvC,KAAK,EAAG,UAAU,EAAE,KAAgB,IAAI,CAAC;gBACzC,WAAW,EAAG,IAAI,EAAE,YAAuB,IAAI,SAAS;aACzD;YACD,OAAO,EAAG,IAAI,CAAC,OAAe,IAAI,EAAE;SACrC,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAC/B,QAAkB,EAClB,QAAgB;QAEhB,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAmC,CAAC;QACzF,MAAM,QAAQ,GAAG,IAAI,EAAE,KAA4C,CAAC;QACpE,MAAM,YAAY,GAAI,QAAQ,EAAE,OAAkB,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;QAEhF,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,OAAO;gBACL,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE;gBAC7D,OAAO,EAAE,EAAE;aACZ,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,WAAW,CACf,IAAI,CAAC,MAAM;gBACT,CAAC,CAAC,2FAA2F;gBAC7F,CAAC,CAAC,qHAAqH,CAC1H,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAC3B,MAAM,kBAAkB,CAAC,2BAA2B,YAAY,EAAE,EAAE;gBAClE,QAAQ;gBACR,MAAM,EAAE,QAAQ,CAAC,MAAM;aACxB,CAAC,CAAC;QACL,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,IAAI,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;gBAChC,MAAM,eAAe,CACnB,gJAAgJ,EAChJ,EAAE,QAAQ,EAAE,CACb,CAAC;YACJ,CAAC;YACD,MAAM,eAAe,CACnB,wBAAwB,YAAY,kGAAkG,EACtI,EAAE,QAAQ,EAAE,CACb,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC,CAAC;IAC/E,CAAC;CACF;AAED,qCAAqC;AAErC,IAAI,QAAoC,CAAC;AAEzC,MAAM,UAAU,kBAAkB;IAChC,QAAQ,GAAG,IAAI,cAAc,CAAC,eAAe,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC,QAAQ;QACX,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IAC3F,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Types for the openFDA service layer.
|
|
3
|
+
* @module services/openfda/types
|
|
4
|
+
*/
|
|
5
|
+
/** Pagination and freshness metadata from an openFDA response. */
|
|
6
|
+
export interface OpenFdaMeta {
|
|
7
|
+
lastUpdated: string;
|
|
8
|
+
limit: number;
|
|
9
|
+
skip: number;
|
|
10
|
+
total: number;
|
|
11
|
+
}
|
|
12
|
+
/** Normalized openFDA API response. */
|
|
13
|
+
export interface OpenFdaResponse<T = Record<string, unknown>> {
|
|
14
|
+
meta: OpenFdaMeta;
|
|
15
|
+
results: T[];
|
|
16
|
+
}
|
|
17
|
+
/** Term-count pair returned by openFDA count queries. */
|
|
18
|
+
export interface OpenFdaCountResult {
|
|
19
|
+
count: number;
|
|
20
|
+
term: string;
|
|
21
|
+
}
|
|
22
|
+
/** Query parameters shared by all openFDA endpoints. */
|
|
23
|
+
export interface OpenFdaQueryParams {
|
|
24
|
+
count?: string | undefined;
|
|
25
|
+
limit?: number | undefined;
|
|
26
|
+
search?: string | undefined;
|
|
27
|
+
skip?: number | undefined;
|
|
28
|
+
sort?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/openfda/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,uCAAuC;AACvC,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,CAAC,EAAE,CAAC;CACd;AAED,yDAAyD;AACzD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/openfda/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cyanheads/openfda-mcp-server",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"mcpName": "io.github.cyanheads/openfda-mcp-server",
|
|
5
|
+
"description": "Query FDA data on drugs, food, devices, and recalls via openFDA. STDIO or Streamable HTTP.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"openfda-mcp-server": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": ["dist/", "README.md", "LICENSE", "CLAUDE.md", "Dockerfile", "server.json"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun run scripts/build.ts",
|
|
15
|
+
"rebuild": "bun run scripts/clean.ts && bun run build",
|
|
16
|
+
"clean": "bun run scripts/clean.ts",
|
|
17
|
+
"devcheck": "bun run scripts/devcheck.ts",
|
|
18
|
+
"tree": "bun run scripts/tree.ts",
|
|
19
|
+
"format": "biome check --write --unsafe",
|
|
20
|
+
"lint:mcp": "bun run scripts/lint-mcp.ts",
|
|
21
|
+
"test": "bunx vitest run",
|
|
22
|
+
"dev:stdio": "MCP_TRANSPORT_TYPE=stdio bun --watch src/index.ts",
|
|
23
|
+
"dev:http": "MCP_TRANSPORT_TYPE=http bun --watch src/index.ts",
|
|
24
|
+
"start:stdio": "MCP_TRANSPORT_TYPE=stdio bun ./dist/index.js",
|
|
25
|
+
"start:http": "MCP_TRANSPORT_TYPE=http bun ./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"mcp-server",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"openfda",
|
|
32
|
+
"fda",
|
|
33
|
+
"drug-safety",
|
|
34
|
+
"adverse-events",
|
|
35
|
+
"typescript"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/cyanheads/openfda-mcp-server.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/cyanheads/openfda-mcp-server#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/cyanheads/openfda-mcp-server/issues"
|
|
44
|
+
},
|
|
45
|
+
"author": "cyanheads <casey@caseyjhand.com> (https://github.com/cyanheads/openfda-mcp-server#readme)",
|
|
46
|
+
"funding": [
|
|
47
|
+
{
|
|
48
|
+
"type": "github",
|
|
49
|
+
"url": "https://github.com/sponsors/cyanheads"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "buy_me_a_coffee",
|
|
53
|
+
"url": "https://www.buymeacoffee.com/cyanheads"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"license": "Apache-2.0",
|
|
57
|
+
"packageManager": "bun@1.3.11",
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22.0.0",
|
|
60
|
+
"bun": ">=1.2.0"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"overrides": {
|
|
66
|
+
"brace-expansion": ">=2.0.3",
|
|
67
|
+
"express-rate-limit": ">=8.2.2",
|
|
68
|
+
"hono": ">=4.12.7",
|
|
69
|
+
"path-to-regexp": ">=8.4.0",
|
|
70
|
+
"picomatch": ">=4.0.4",
|
|
71
|
+
"yaml": ">=2.8.3"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@cyanheads/mcp-ts-core": "^0.2.12",
|
|
75
|
+
"pino-pretty": "^13.1.3"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@biomejs/biome": "^2.4.10",
|
|
79
|
+
"@opentelemetry/api": "^1.9.1",
|
|
80
|
+
"@types/node": "^25.5.2",
|
|
81
|
+
"@vitest/coverage-istanbul": "^4.1.2",
|
|
82
|
+
"depcheck": "^1.4.7",
|
|
83
|
+
"ignore": "^7.0.5",
|
|
84
|
+
"tsc-alias": "^1.8.16",
|
|
85
|
+
"typescript": "^5.9.3",
|
|
86
|
+
"vitest": "^4.1.2"
|
|
87
|
+
}
|
|
88
|
+
}
|