feat: add MCP service with 5 multi-level retrieval tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 13:29:10 +08:00
parent a191a4db00
commit ac60f0bb49
10 changed files with 355 additions and 2 deletions

View File

@@ -11,13 +11,15 @@
"dependencies": { "dependencies": {
"@agent-fox/shared": "workspace:*", "@agent-fox/shared": "workspace:*",
"@modelcontextprotocol/sdk": "^1.12.0", "@modelcontextprotocol/sdk": "^1.12.0",
"express": "^5.0.0", "bcrypt": "^6.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.0.0",
"zod": "^3.24.0" "zod": "^3.24.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5.0.0", "@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"tsx": "^4.19.0", "tsx": "^4.19.0",
"typescript": "^5.7.0" "typescript": "^5.7.0"
} }

33
packages/mcp/src/auth.ts Normal file
View File

@@ -0,0 +1,33 @@
import type { Request, Response, NextFunction } from 'express';
import bcrypt from 'bcrypt';
import { prisma } from '@agent-fox/shared';
export async function mcpAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
const projectId = req.params['projectId'] as string;
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
res.status(401).json({ error: 'Missing API key' });
return;
}
const apiKey = header.slice(7);
const project = await prisma.project.findUnique({
where: { id: projectId },
select: { id: true, apiKeyHash: true },
});
if (!project) {
res.status(404).json({ error: 'Project not found' });
return;
}
const valid = await bcrypt.compare(apiKey, project.apiKeyHash);
if (!valid) {
res.status(401).json({ error: 'Invalid API key' });
return;
}
(req as any).projectId = projectId;
next();
}

View File

@@ -1,5 +1,9 @@
import { randomUUID } from 'node:crypto';
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { mcpAuth } from './auth.js';
import { createMcpServer } from './server.js';
const app = express(); const app = express();
app.use(cors()); app.use(cors());
@@ -9,6 +13,60 @@ app.get('/health', (_req, res) => {
res.json({ status: 'ok' }); res.json({ status: 'ok' });
}); });
// Session storage
const transports: Record<string, StreamableHTTPServerTransport> = {};
// MCP Streamable HTTP endpoint
app.post('/mcp/:projectId', mcpAuth, async (req, res) => {
const projectId = (req as any).projectId as string;
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (sessionId && transports[sessionId]) {
await transports[sessionId].handleRequest(req, res, req.body);
return;
}
// New session — check for initialize request
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
transports[id] = transport;
},
});
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = createMcpServer(projectId);
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// SSE endpoint for session resumption
app.get('/mcp/:projectId', mcpAuth, async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string;
if (sessionId && transports[sessionId]) {
await transports[sessionId].handleRequest(req, res);
} else {
res.status(400).json({ error: 'Invalid session. Start a new session via POST.' });
}
});
// Session termination
app.delete('/mcp/:projectId', mcpAuth, async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string;
if (sessionId && transports[sessionId]) {
await transports[sessionId].close();
delete transports[sessionId];
res.status(204).end();
} else {
res.status(400).json({ error: 'Invalid session' });
}
});
const port = process.env.MCP_PORT || 3001; const port = process.env.MCP_PORT || 3001;
app.listen(port, () => { app.listen(port, () => {
console.log(`MCP service running on port ${port}`); console.log(`MCP service running on port ${port}`);

View File

@@ -0,0 +1,54 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { getProjectOverview } from './tools/get-project-overview.js';
import { listModules } from './tools/list-modules.js';
import { listEndpoints } from './tools/list-endpoints.js';
import { getEndpointDetail } from './tools/get-endpoint-detail.js';
import { searchEndpoints } from './tools/search-endpoints.js';
export function createMcpServer(projectId: string): McpServer {
const server = new McpServer({
name: 'agent-fox',
version: '0.1.0',
});
server.tool(
'get_project_overview',
'Get an overview of this API project including its name, version, base URL, and a summary of available modules with endpoint counts. Call this first to understand what the API offers.',
{},
async () => getProjectOverview(projectId),
);
server.tool(
'list_modules',
'List all API modules/groups with their descriptions. Each module contains related endpoints. Use this when you need module descriptions to decide which module to explore.',
{},
async () => listModules(projectId),
);
server.tool(
'list_endpoints',
'List all endpoints in a specific module. Returns method, path, and summary for each endpoint. Use get_endpoint_detail to get full information about a specific endpoint.',
{ moduleId: z.string().describe('The module ID to list endpoints for. Get module IDs from get_project_overview or list_modules.') },
async ({ moduleId }) => listEndpoints(projectId, moduleId),
);
server.tool(
'get_endpoint_detail',
'Get complete details for a specific endpoint including parameters, request body schema, response schemas. Use this when you need to understand exactly how to call an endpoint.',
{ endpointId: z.string().describe('The endpoint ID. Get endpoint IDs from list_endpoints or search_endpoints.') },
async ({ endpointId }) => getEndpointDetail(projectId, endpointId),
);
server.tool(
'search_endpoints',
'Search for endpoints by keyword. Searches across path, summary, description, and operationId. Optionally filter by module. Returns matching endpoint summaries.',
{
keyword: z.string().describe('Search keyword to match against endpoint path, summary, description, and operationId.'),
moduleId: z.string().optional().describe('Optional module ID to limit search scope. Omit to search all modules.'),
},
async ({ keyword, moduleId }) => searchEndpoints(projectId, keyword, moduleId),
);
return server;
}

View File

@@ -0,0 +1,22 @@
import { prisma } from '@agent-fox/shared';
export async function getEndpointDetail(projectId: string, endpointId: string) {
const endpoint = await prisma.endpoint.findFirst({
where: { id: endpointId, projectId },
include: { module: { select: { name: true } } },
});
if (!endpoint) {
return { content: [{ type: 'text' as const, text: `Endpoint "${endpointId}" not found. Use list_endpoints to see available endpoints.` }], isError: true };
}
const detail = {
id: endpoint.id, method: endpoint.method, path: endpoint.path,
summary: endpoint.summary, description: endpoint.description,
operationId: endpoint.operationId, moduleName: endpoint.module.name,
parameters: endpoint.parameters, requestBody: endpoint.requestBody,
responses: endpoint.responses, deprecated: endpoint.deprecated,
};
return { content: [{ type: 'text' as const, text: JSON.stringify(detail, null, 2) }] };
}

View File

@@ -0,0 +1,32 @@
import { prisma } from '@agent-fox/shared';
export async function getProjectOverview(projectId: string) {
const project = await prisma.project.findUnique({
where: { id: projectId },
select: {
name: true, description: true, openApiVersion: true, baseUrl: true,
modules: {
select: { id: true, name: true, _count: { select: { endpoints: true } } },
orderBy: { sortOrder: 'asc' },
},
_count: { select: { endpoints: true } },
},
});
if (!project) {
return { content: [{ type: 'text' as const, text: 'Project not found' }], isError: true };
}
const overview = {
name: project.name,
description: project.description,
version: project.openApiVersion,
baseUrl: project.baseUrl,
totalEndpoints: project._count.endpoints,
modules: project.modules.map((m) => ({
id: m.id, name: m.name, endpointCount: m._count.endpoints,
})),
};
return { content: [{ type: 'text' as const, text: JSON.stringify(overview, null, 2) }] };
}

View File

@@ -0,0 +1,16 @@
import { prisma } from '@agent-fox/shared';
export async function listEndpoints(projectId: string, moduleId: string) {
const mod = await prisma.module.findFirst({ where: { id: moduleId, projectId } });
if (!mod) {
return { content: [{ type: 'text' as const, text: `Module "${moduleId}" not found. Use get_project_overview or list_modules to see available modules.` }], isError: true };
}
const endpoints = await prisma.endpoint.findMany({
where: { projectId, moduleId },
select: { id: true, method: true, path: true, summary: true, deprecated: true },
orderBy: [{ path: 'asc' }, { method: 'asc' }],
});
return { content: [{ type: 'text' as const, text: JSON.stringify(endpoints, null, 2) }] };
}

View File

@@ -0,0 +1,15 @@
import { prisma } from '@agent-fox/shared';
export async function listModules(projectId: string) {
const modules = await prisma.module.findMany({
where: { projectId },
select: { id: true, name: true, description: true, _count: { select: { endpoints: true } } },
orderBy: { sortOrder: 'asc' },
});
const result = modules.map((m) => ({
id: m.id, name: m.name, description: m.description, endpointCount: m._count.endpoints,
}));
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
}

View File

@@ -0,0 +1,34 @@
import { prisma } from '@agent-fox/shared';
export async function searchEndpoints(projectId: string, keyword: string, moduleId?: string) {
const where: any = { projectId };
if (moduleId) where.moduleId = moduleId;
where.OR = [
{ path: { contains: keyword, mode: 'insensitive' } },
{ summary: { contains: keyword, mode: 'insensitive' } },
{ description: { contains: keyword, mode: 'insensitive' } },
{ operationId: { contains: keyword, mode: 'insensitive' } },
];
const endpoints = await prisma.endpoint.findMany({
where,
select: {
id: true, method: true, path: true, summary: true, deprecated: true,
module: { select: { name: true } },
},
orderBy: [{ path: 'asc' }, { method: 'asc' }],
take: 20,
});
if (endpoints.length === 0) {
return { content: [{ type: 'text' as const, text: `No endpoints found matching "${keyword}". Try a different keyword or use list_modules to browse.` }] };
}
const result = endpoints.map((e) => ({
id: e.id, method: e.method, path: e.path, summary: e.summary,
moduleName: e.module.name, deprecated: e.deprecated,
}));
return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
}

87
pnpm-lock.yaml generated
View File

@@ -24,6 +24,9 @@ importers:
'@modelcontextprotocol/sdk': '@modelcontextprotocol/sdk':
specifier: ^1.12.0 specifier: ^1.12.0
version: 1.29.0(zod@3.25.76) version: 1.29.0(zod@3.25.76)
bcrypt:
specifier: ^6.0.0
version: 6.0.0
cors: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: 2.8.6 version: 2.8.6
@@ -34,6 +37,9 @@ importers:
specifier: ^3.24.0 specifier: ^3.24.0
version: 3.25.76 version: 3.25.76
devDependencies: devDependencies:
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
'@types/cors': '@types/cors':
specifier: ^2.8.17 specifier: ^2.8.17
version: 2.8.19 version: 2.8.19
@@ -52,6 +58,9 @@ importers:
'@agent-fox/shared': '@agent-fox/shared':
specifier: workspace:* specifier: workspace:*
version: link:../shared version: link:../shared
'@apidevtools/swagger-parser':
specifier: ^12.1.0
version: 12.1.0(openapi-types@12.1.3)
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
@@ -64,6 +73,9 @@ importers:
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.3 specifier: ^9.0.3
version: 9.0.3 version: 9.0.3
openapi-types:
specifier: ^12.1.3
version: 12.1.3
zod: zod:
specifier: ^3.24.0 specifier: ^3.24.0
version: 3.25.76 version: 3.25.76
@@ -111,6 +123,22 @@ importers:
packages: packages:
'@apidevtools/json-schema-ref-parser@14.0.1':
resolution: {integrity: sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==}
engines: {node: '>= 16'}
'@apidevtools/openapi-schemas@2.1.0':
resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==}
engines: {node: '>=10'}
'@apidevtools/swagger-methods@3.0.2':
resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
'@apidevtools/swagger-parser@12.1.0':
resolution: {integrity: sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==}
peerDependencies:
openapi-types: '>=7'
'@emnapi/core@1.9.1': '@emnapi/core@1.9.1':
resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
@@ -456,6 +484,9 @@ packages:
'@types/http-errors@2.0.5': '@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
@@ -481,6 +512,14 @@ packages:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
ajv: ^8.5.0
peerDependenciesMeta:
ajv:
optional: true
ajv-formats@3.0.1: ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies: peerDependencies:
@@ -492,6 +531,9 @@ packages:
ajv@8.18.0: ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
bcrypt@6.0.0: bcrypt@6.0.0:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
@@ -523,6 +565,9 @@ packages:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
call-me-maybe@1.0.2:
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -763,6 +808,10 @@ packages:
jose@6.2.2: jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
json-schema-traverse@1.0.0: json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
@@ -940,6 +989,9 @@ packages:
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
parseurl@1.3.3: parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -1182,6 +1234,25 @@ packages:
snapshots: snapshots:
'@apidevtools/json-schema-ref-parser@14.0.1':
dependencies:
'@types/json-schema': 7.0.15
js-yaml: 4.1.1
'@apidevtools/openapi-schemas@2.1.0': {}
'@apidevtools/swagger-methods@3.0.2': {}
'@apidevtools/swagger-parser@12.1.0(openapi-types@12.1.3)':
dependencies:
'@apidevtools/json-schema-ref-parser': 14.0.1
'@apidevtools/openapi-schemas': 2.1.0
'@apidevtools/swagger-methods': 3.0.2
ajv: 8.18.0
ajv-draft-04: 1.0.0(ajv@8.18.0)
call-me-maybe: 1.0.2
openapi-types: 12.1.3
'@emnapi/core@1.9.1': '@emnapi/core@1.9.1':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.2.0 '@emnapi/wasi-threads': 1.2.0
@@ -1437,6 +1508,8 @@ snapshots:
'@types/http-errors@2.0.5': {} '@types/http-errors@2.0.5': {}
'@types/json-schema@7.0.15': {}
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
@@ -1466,6 +1539,10 @@ snapshots:
mime-types: 3.0.2 mime-types: 3.0.2
negotiator: 1.0.0 negotiator: 1.0.0
ajv-draft-04@1.0.0(ajv@8.18.0):
optionalDependencies:
ajv: 8.18.0
ajv-formats@3.0.1(ajv@8.18.0): ajv-formats@3.0.1(ajv@8.18.0):
optionalDependencies: optionalDependencies:
ajv: 8.18.0 ajv: 8.18.0
@@ -1477,6 +1554,8 @@ snapshots:
json-schema-traverse: 1.0.0 json-schema-traverse: 1.0.0
require-from-string: 2.0.2 require-from-string: 2.0.2
argparse@2.0.1: {}
bcrypt@6.0.0: bcrypt@6.0.0:
dependencies: dependencies:
node-addon-api: 8.7.0 node-addon-api: 8.7.0
@@ -1525,6 +1604,8 @@ snapshots:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
call-me-maybe@1.0.2: {}
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@@ -1781,6 +1862,10 @@ snapshots:
jose@6.2.2: {} jose@6.2.2: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
json-schema-traverse@1.0.0: {} json-schema-traverse@1.0.0: {}
json-schema-typed@8.0.2: {} json-schema-typed@8.0.2: {}
@@ -1916,6 +2001,8 @@ snapshots:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
openapi-types@12.1.3: {}
parseurl@1.3.3: {} parseurl@1.3.3: {}
path-key@3.1.1: {} path-key@3.1.1: {}