@upstash/context7-mcp 1.0.13 → 1.0.15
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/README.md +674 -88
- package/dist/index.js +131 -51
- package/dist/lib/api.js +31 -11
- package/dist/lib/encryption.js +35 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,39 +4,82 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { searchLibraries, fetchLibraryDocumentation } from "./lib/api.js";
|
|
6
6
|
import { formatSearchResults } from "./lib/utils.js";
|
|
7
|
-
import dotenv from "dotenv";
|
|
8
7
|
import { createServer } from "http";
|
|
9
8
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
9
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
const DEFAULT_MINIMUM_TOKENS = 10000;
|
|
12
|
+
// Parse CLI arguments using commander
|
|
13
|
+
const program = new Command()
|
|
14
|
+
.option("--transport <stdio|http>", "transport type", "stdio")
|
|
15
|
+
.option("--port <number>", "port for HTTP transport", "3000")
|
|
16
|
+
.option("--api-key <key>", "API key for authentication")
|
|
17
|
+
.allowUnknownOption() // let MCP Inspector / other wrappers pass through extra flags
|
|
18
|
+
.parse(process.argv);
|
|
19
|
+
const cliOptions = program.opts();
|
|
20
|
+
// Validate transport option
|
|
21
|
+
const allowedTransports = ["stdio", "http"];
|
|
22
|
+
if (!allowedTransports.includes(cliOptions.transport)) {
|
|
23
|
+
console.error(`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http.`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// Transport configuration
|
|
27
|
+
const TRANSPORT_TYPE = (cliOptions.transport || "stdio");
|
|
28
|
+
// Disallow incompatible flags based on transport
|
|
29
|
+
const passedPortFlag = process.argv.includes("--port");
|
|
30
|
+
const passedApiKeyFlag = process.argv.includes("--api-key");
|
|
31
|
+
if (TRANSPORT_TYPE === "http" && passedApiKeyFlag) {
|
|
32
|
+
console.error("The --api-key flag is not allowed when using --transport http. Use header-based auth at the HTTP layer instead.");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (TRANSPORT_TYPE === "stdio" && passedPortFlag) {
|
|
36
|
+
console.error("The --port flag is not allowed when using --transport stdio.");
|
|
37
|
+
process.exit(1);
|
|
24
38
|
}
|
|
39
|
+
// HTTP port configuration
|
|
40
|
+
const CLI_PORT = (() => {
|
|
41
|
+
const parsed = parseInt(cliOptions.port, 10);
|
|
42
|
+
return isNaN(parsed) ? undefined : parsed;
|
|
43
|
+
})();
|
|
25
44
|
// Store SSE transports by session ID
|
|
26
45
|
const sseTransports = {};
|
|
46
|
+
function getClientIp(req) {
|
|
47
|
+
// Check both possible header casings
|
|
48
|
+
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
49
|
+
if (forwardedFor) {
|
|
50
|
+
// X-Forwarded-For can contain multiple IPs
|
|
51
|
+
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
52
|
+
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
53
|
+
// Find the first public IP address
|
|
54
|
+
for (const ip of ipList) {
|
|
55
|
+
const plainIp = ip.replace(/^::ffff:/, "");
|
|
56
|
+
if (!plainIp.startsWith("10.") &&
|
|
57
|
+
!plainIp.startsWith("192.168.") &&
|
|
58
|
+
!/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp)) {
|
|
59
|
+
return plainIp;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// If all are private, use the first one
|
|
63
|
+
return ipList[0].replace(/^::ffff:/, "");
|
|
64
|
+
}
|
|
65
|
+
// Fallback: use remote address, strip IPv6-mapped IPv4
|
|
66
|
+
if (req.socket?.remoteAddress) {
|
|
67
|
+
return req.socket.remoteAddress.replace(/^::ffff:/, "");
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
27
71
|
// Function to create a new server instance with all tools registered
|
|
28
|
-
function createServerInstance() {
|
|
72
|
+
function createServerInstance(clientIp, apiKey) {
|
|
29
73
|
const server = new McpServer({
|
|
30
74
|
name: "Context7",
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
resources: {},
|
|
35
|
-
tools: {},
|
|
36
|
-
},
|
|
75
|
+
version: "1.0.15",
|
|
76
|
+
}, {
|
|
77
|
+
instructions: "Use this server to retrieve up-to-date documentation and code examples for any library.",
|
|
37
78
|
});
|
|
38
79
|
// Register Context7 tools
|
|
39
|
-
server.
|
|
80
|
+
server.registerTool("resolve-library-id", {
|
|
81
|
+
title: "Resolve Context7 Library ID",
|
|
82
|
+
description: `Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries.
|
|
40
83
|
|
|
41
84
|
You MUST call this function before 'get-library-docs' to obtain a valid Context7-compatible library ID UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.
|
|
42
85
|
|
|
@@ -54,12 +97,14 @@ Response Format:
|
|
|
54
97
|
- If multiple good matches exist, acknowledge this but proceed with the most relevant one
|
|
55
98
|
- If no good matches exist, clearly state this and suggest query refinements
|
|
56
99
|
|
|
57
|
-
For ambiguous queries, request clarification before proceeding with a best-guess match.`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
For ambiguous queries, request clarification before proceeding with a best-guess match.`,
|
|
101
|
+
inputSchema: {
|
|
102
|
+
libraryName: z
|
|
103
|
+
.string()
|
|
104
|
+
.describe("Library name to search for and retrieve a Context7-compatible library ID."),
|
|
105
|
+
},
|
|
61
106
|
}, async ({ libraryName }) => {
|
|
62
|
-
const searchResponse = await searchLibraries(libraryName);
|
|
107
|
+
const searchResponse = await searchLibraries(libraryName, clientIp, apiKey);
|
|
63
108
|
if (!searchResponse.results || searchResponse.results.length === 0) {
|
|
64
109
|
return {
|
|
65
110
|
content: [
|
|
@@ -96,24 +141,28 @@ ${resultsText}`,
|
|
|
96
141
|
],
|
|
97
142
|
};
|
|
98
143
|
});
|
|
99
|
-
server.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
server.registerTool("get-library-docs", {
|
|
145
|
+
title: "Get Library Docs",
|
|
146
|
+
description: "Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool, UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
context7CompatibleLibraryID: z
|
|
149
|
+
.string()
|
|
150
|
+
.describe("Exact Context7-compatible library ID (e.g., '/mongodb/docs', '/vercel/next.js', '/supabase/supabase', '/vercel/next.js/v14.3.0-canary.87') retrieved from 'resolve-library-id' or directly from user query in the format '/org/project' or '/org/project/version'."),
|
|
151
|
+
topic: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Topic to focus documentation on (e.g., 'hooks', 'routing')."),
|
|
155
|
+
tokens: z
|
|
156
|
+
.preprocess((val) => (typeof val === "string" ? Number(val) : val), z.number())
|
|
157
|
+
.transform((val) => (val < DEFAULT_MINIMUM_TOKENS ? DEFAULT_MINIMUM_TOKENS : val))
|
|
158
|
+
.optional()
|
|
159
|
+
.describe(`Maximum number of tokens of documentation to retrieve (default: ${DEFAULT_MINIMUM_TOKENS}). Higher values provide more context but consume more tokens.`),
|
|
160
|
+
},
|
|
112
161
|
}, async ({ context7CompatibleLibraryID, tokens = DEFAULT_MINIMUM_TOKENS, topic = "" }) => {
|
|
113
162
|
const fetchDocsResponse = await fetchLibraryDocumentation(context7CompatibleLibraryID, {
|
|
114
163
|
tokens,
|
|
115
164
|
topic,
|
|
116
|
-
});
|
|
165
|
+
}, clientIp, apiKey);
|
|
117
166
|
if (!fetchDocsResponse) {
|
|
118
167
|
return {
|
|
119
168
|
content: [
|
|
@@ -136,27 +185,58 @@ ${resultsText}`,
|
|
|
136
185
|
return server;
|
|
137
186
|
}
|
|
138
187
|
async function main() {
|
|
139
|
-
const transportType =
|
|
140
|
-
if (transportType === "http"
|
|
188
|
+
const transportType = TRANSPORT_TYPE;
|
|
189
|
+
if (transportType === "http") {
|
|
141
190
|
// Get initial port from environment or use default
|
|
142
|
-
const initialPort =
|
|
191
|
+
const initialPort = CLI_PORT ?? 3000;
|
|
143
192
|
// Keep track of which port we end up using
|
|
144
193
|
let actualPort = initialPort;
|
|
145
194
|
const httpServer = createServer(async (req, res) => {
|
|
146
|
-
const url =
|
|
195
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`).pathname;
|
|
147
196
|
// Set CORS headers for all responses
|
|
148
197
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
149
198
|
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
|
|
150
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id,
|
|
199
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id, MCP-Protocol-Version, X-Context7-API-Key, Context7-API-Key, X-API-Key, Authorization");
|
|
200
|
+
res.setHeader("Access-Control-Expose-Headers", "MCP-Session-Id");
|
|
151
201
|
// Handle preflight OPTIONS requests
|
|
152
202
|
if (req.method === "OPTIONS") {
|
|
153
203
|
res.writeHead(200);
|
|
154
204
|
res.end();
|
|
155
205
|
return;
|
|
156
206
|
}
|
|
207
|
+
// Function to extract header value safely, handling both string and string[] cases
|
|
208
|
+
const extractHeaderValue = (value) => {
|
|
209
|
+
if (!value)
|
|
210
|
+
return undefined;
|
|
211
|
+
return typeof value === "string" ? value : value[0];
|
|
212
|
+
};
|
|
213
|
+
// Extract Authorization header and remove Bearer prefix if present
|
|
214
|
+
const extractBearerToken = (authHeader) => {
|
|
215
|
+
const header = extractHeaderValue(authHeader);
|
|
216
|
+
if (!header)
|
|
217
|
+
return undefined;
|
|
218
|
+
// If it starts with 'Bearer ', remove that prefix
|
|
219
|
+
if (header.startsWith("Bearer ")) {
|
|
220
|
+
return header.substring(7).trim();
|
|
221
|
+
}
|
|
222
|
+
// Otherwise return the raw value
|
|
223
|
+
return header;
|
|
224
|
+
};
|
|
225
|
+
// Check headers in order of preference
|
|
226
|
+
const apiKey = extractBearerToken(req.headers.authorization) ||
|
|
227
|
+
extractHeaderValue(req.headers["Context7-API-Key"]) ||
|
|
228
|
+
extractHeaderValue(req.headers["X-API-Key"]) ||
|
|
229
|
+
extractHeaderValue(req.headers["context7-api-key"]) ||
|
|
230
|
+
extractHeaderValue(req.headers["x-api-key"]) ||
|
|
231
|
+
extractHeaderValue(req.headers["Context7_API_Key"]) ||
|
|
232
|
+
extractHeaderValue(req.headers["X_API_Key"]) ||
|
|
233
|
+
extractHeaderValue(req.headers["context7_api_key"]) ||
|
|
234
|
+
extractHeaderValue(req.headers["x_api_key"]);
|
|
157
235
|
try {
|
|
236
|
+
// Extract client IP address using socket remote address (most reliable)
|
|
237
|
+
const clientIp = getClientIp(req);
|
|
158
238
|
// Create new server instance for each request
|
|
159
|
-
const requestServer = createServerInstance();
|
|
239
|
+
const requestServer = createServerInstance(clientIp, apiKey);
|
|
160
240
|
if (url === "/mcp") {
|
|
161
241
|
const transport = new StreamableHTTPServerTransport({
|
|
162
242
|
sessionIdGenerator: undefined,
|
|
@@ -177,8 +257,8 @@ async function main() {
|
|
|
177
257
|
}
|
|
178
258
|
else if (url === "/messages" && req.method === "POST") {
|
|
179
259
|
// Get session ID from query parameters
|
|
180
|
-
const
|
|
181
|
-
|
|
260
|
+
const sessionId = new URL(req.url || "", `http://${req.headers.host}`).searchParams.get("sessionId") ??
|
|
261
|
+
"";
|
|
182
262
|
if (!sessionId) {
|
|
183
263
|
res.writeHead(400);
|
|
184
264
|
res.end("Missing sessionId parameter");
|
|
@@ -225,7 +305,7 @@ async function main() {
|
|
|
225
305
|
});
|
|
226
306
|
httpServer.listen(port, () => {
|
|
227
307
|
actualPort = port;
|
|
228
|
-
console.error(`Context7 Documentation MCP Server running on ${transportType.toUpperCase()} at http://localhost:${actualPort}/mcp
|
|
308
|
+
console.error(`Context7 Documentation MCP Server running on ${transportType.toUpperCase()} at http://localhost:${actualPort}/mcp with SSE endpoint at /sse`);
|
|
229
309
|
});
|
|
230
310
|
};
|
|
231
311
|
// Start the server with initial port
|
|
@@ -233,7 +313,7 @@ async function main() {
|
|
|
233
313
|
}
|
|
234
314
|
else {
|
|
235
315
|
// Stdio transport - this is already stateless by nature
|
|
236
|
-
const server = createServerInstance();
|
|
316
|
+
const server = createServerInstance(undefined, cliOptions.apiKey);
|
|
237
317
|
const transport = new StdioServerTransport();
|
|
238
318
|
await server.connect(transport);
|
|
239
319
|
console.error("Context7 Documentation MCP Server running on stdio");
|
package/dist/lib/api.js
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
+
import { generateHeaders } from "./encryption.js";
|
|
1
2
|
const CONTEXT7_API_BASE_URL = "https://context7.com/api";
|
|
2
3
|
const DEFAULT_TYPE = "txt";
|
|
3
4
|
/**
|
|
4
5
|
* Searches for libraries matching the given query
|
|
5
6
|
* @param query The search query
|
|
7
|
+
* @param clientIp Optional client IP address to include in headers
|
|
8
|
+
* @param apiKey Optional API key for authentication
|
|
6
9
|
* @returns Search results or null if the request fails
|
|
7
10
|
*/
|
|
8
|
-
export async function searchLibraries(query) {
|
|
11
|
+
export async function searchLibraries(query, clientIp, apiKey) {
|
|
9
12
|
try {
|
|
10
13
|
const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`);
|
|
11
14
|
url.searchParams.set("query", query);
|
|
12
|
-
const
|
|
15
|
+
const headers = generateHeaders(clientIp, apiKey);
|
|
16
|
+
const response = await fetch(url, { headers });
|
|
13
17
|
if (!response.ok) {
|
|
14
18
|
const errorCode = response.status;
|
|
15
19
|
if (errorCode === 429) {
|
|
16
|
-
console.error(
|
|
20
|
+
console.error("Rate limited due to too many requests. Please try again later.");
|
|
17
21
|
return {
|
|
18
22
|
results: [],
|
|
19
|
-
error:
|
|
23
|
+
error: "Rate limited due to too many requests. Please try again later.",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (errorCode === 401) {
|
|
27
|
+
console.error("Unauthorized. Please check your API key.");
|
|
28
|
+
return {
|
|
29
|
+
results: [],
|
|
30
|
+
error: "Unauthorized. Please check your API key.",
|
|
20
31
|
};
|
|
21
32
|
}
|
|
22
33
|
console.error(`Failed to search libraries. Please try again later. Error code: ${errorCode}`);
|
|
@@ -36,9 +47,11 @@ export async function searchLibraries(query) {
|
|
|
36
47
|
* Fetches documentation context for a specific library
|
|
37
48
|
* @param libraryId The library ID to fetch documentation for
|
|
38
49
|
* @param options Options for the request
|
|
50
|
+
* @param clientIp Optional client IP address to include in headers
|
|
51
|
+
* @param apiKey Optional API key for authentication
|
|
39
52
|
* @returns The documentation text or null if the request fails
|
|
40
53
|
*/
|
|
41
|
-
export async function fetchLibraryDocumentation(libraryId, options = {}) {
|
|
54
|
+
export async function fetchLibraryDocumentation(libraryId, options = {}, clientIp, apiKey) {
|
|
42
55
|
try {
|
|
43
56
|
if (libraryId.startsWith("/")) {
|
|
44
57
|
libraryId = libraryId.slice(1);
|
|
@@ -49,15 +62,22 @@ export async function fetchLibraryDocumentation(libraryId, options = {}) {
|
|
|
49
62
|
if (options.topic)
|
|
50
63
|
url.searchParams.set("topic", options.topic);
|
|
51
64
|
url.searchParams.set("type", DEFAULT_TYPE);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
"X-Context7-Source": "mcp-server",
|
|
55
|
-
},
|
|
56
|
-
});
|
|
65
|
+
const headers = generateHeaders(clientIp, apiKey, { "X-Context7-Source": "mcp-server" });
|
|
66
|
+
const response = await fetch(url, { headers });
|
|
57
67
|
if (!response.ok) {
|
|
58
68
|
const errorCode = response.status;
|
|
59
69
|
if (errorCode === 429) {
|
|
60
|
-
const errorMessage =
|
|
70
|
+
const errorMessage = "Rate limited due to too many requests. Please try again later.";
|
|
71
|
+
console.error(errorMessage);
|
|
72
|
+
return errorMessage;
|
|
73
|
+
}
|
|
74
|
+
if (errorCode === 404) {
|
|
75
|
+
const errorMessage = "The library you are trying to access does not exist. Please try with a different library ID.";
|
|
76
|
+
console.error(errorMessage);
|
|
77
|
+
return errorMessage;
|
|
78
|
+
}
|
|
79
|
+
if (errorCode === 401) {
|
|
80
|
+
const errorMessage = "Unauthorized. Please check your API key.";
|
|
61
81
|
console.error(errorMessage);
|
|
62
82
|
return errorMessage;
|
|
63
83
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createCipheriv, randomBytes } from "crypto";
|
|
2
|
+
const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY ||
|
|
3
|
+
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
|
4
|
+
const ALGORITHM = "aes-256-cbc";
|
|
5
|
+
function validateEncryptionKey(key) {
|
|
6
|
+
// Must be exactly 64 hex characters (32 bytes)
|
|
7
|
+
return /^[0-9a-fA-F]{64}$/.test(key);
|
|
8
|
+
}
|
|
9
|
+
function encryptClientIp(clientIp) {
|
|
10
|
+
if (!validateEncryptionKey(ENCRYPTION_KEY)) {
|
|
11
|
+
console.error("Invalid encryption key format. Must be 64 hex characters.");
|
|
12
|
+
return clientIp; // Fallback to unencrypted
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const iv = randomBytes(16);
|
|
16
|
+
const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv);
|
|
17
|
+
let encrypted = cipher.update(clientIp, "utf8", "hex");
|
|
18
|
+
encrypted += cipher.final("hex");
|
|
19
|
+
return iv.toString("hex") + ":" + encrypted;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error("Error encrypting client IP:", error);
|
|
23
|
+
return clientIp; // Fallback to unencrypted
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function generateHeaders(clientIp, apiKey, extraHeaders = {}) {
|
|
27
|
+
const headers = { ...extraHeaders };
|
|
28
|
+
if (clientIp) {
|
|
29
|
+
headers["mcp-client-ip"] = encryptClientIp(clientIp);
|
|
30
|
+
}
|
|
31
|
+
if (apiKey) {
|
|
32
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
33
|
+
}
|
|
34
|
+
return headers;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@upstash/context7-mcp","version":"
|
|
1
|
+
{"name":"@upstash/context7-mcp","version":"1.0.15","description":"MCP server for Context7","scripts":{"test":"echo \"Error: no test specified\" && exit 1","build":"tsc && chmod 755 dist/index.js","format":"prettier --write .","lint":"eslint \"**/*.{js,ts,tsx}\" --fix","lint:check":"eslint \"**/*.{js,ts,tsx}\"","start":"node dist/index.js --transport http","pack-dxt":"bun install && bun run build && rm -rf node_modules && bun install --production && mv dxt/.dxtignore .dxtignore && mv dxt/manifest.json manifest.json && dxt validate manifest.json && dxt pack . dxt/context7.dxt && mv manifest.json dxt/manifest.json && mv .dxtignore dxt/.dxtignore && bun install"},"repository":{"type":"git","url":"git+https://github.com/upstash/context7.git"},"keywords":["modelcontextprotocol","mcp","context7"],"author":"abdush","license":"MIT","type":"module","bin":{"context7-mcp":"dist/index.js"},"files":["dist"],"bugs":{"url":"https://github.com/upstash/context7/issues"},"homepage":"https://github.com/upstash/context7#readme","dependencies":{"@modelcontextprotocol/sdk":"^1.13.2","commander":"^14.0.0","zod":"^3.24.2"},"devDependencies":{"@types/node":"^22.13.14","@typescript-eslint/eslint-plugin":"^8.28.0","@typescript-eslint/parser":"^8.28.0","eslint":"^9.23.0","eslint-config-prettier":"^10.1.1","eslint-plugin-prettier":"^5.2.5","prettier":"^3.5.3","typescript":"^5.8.2","typescript-eslint":"^8.28.0"}}
|