diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9192b2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules +npm-debug.log + +# Build output +dist + +# Tests +src/tests +*.test.ts +coverage + +# Git +.git +.gitignore + +# Documentation +*.md +!README.md + +# IDE +.vscode +.idea +.cursor + +# Environment files +.env +.env.local +.env.*.local + +# macOS +.DS_Store + +# Logs +logs +*.log + +# Temporary files +tmp +temp +*.tmp diff --git a/.gitignore b/.gitignore index 58c9f8d..d3f930b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ logs data coverage .claude +.smithery diff --git a/CLAUDE.md b/CLAUDE.md index 8c7b791..e650150 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ```bash npm install # Install dependencies npm run build # Compile TypeScript to ./dist -npm start # Start the server +npm start # Start the server in stdio mode (default) +npm run start:http # Start the server in HTTP mode on port 3000 npm run watch # Development build with file watching npm run debug # Debug with MCP inspector npm test # Run tests @@ -15,6 +16,48 @@ npm run test:watch # Run tests with file watching npm run test:coverage # Run tests with coverage report ``` +## Running the Server + +### Stdio Mode (Local/Default) +The default mode runs the server using stdio transport for local MCP clients: +```bash +npm start +# or +node dist/index.js +``` + +### HTTP Mode (Remote Hosting) +Run as an HTTP server with SSE (Server-Sent Events) transport: +```bash +# Default port 3000 +npm run start:http + +# Custom port +node dist/index.js --http --port 8080 +``` + +When running in HTTP mode, the following endpoints are available: +- `POST /sse` - MCP SSE endpoint for client connections +- `GET /health` - Health check endpoint +- `POST /message` - Message endpoint (handled by SSE transport) + +### Docker Mode +Run as a Docker container (runs HTTP mode on port 80 by default): +```bash +# Build and run +docker build -t ynab-mcp-server . +docker run -d -p 80:80 -e YNAB_API_TOKEN=your_token --name ynab-mcp ynab-mcp-server + +# Health check +curl http://localhost/health + +# View logs +docker logs ynab-mcp + +# Map to different host port +docker run -d -p 3000:80 -e YNAB_API_TOKEN=your_token --name ynab-mcp ynab-mcp-server +``` + ## Git Best Practices ALWAYS use conventional commits format (Refer to https://www.conventionalcommits.org/en/v1.0.0/) when creating git commit messages. diff --git a/Dockerfile b/Dockerfile index 7ff65fe..446cd1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,12 +14,8 @@ COPY . . # Build the project RUN npm run build -# Expose any port if required (not strictly necessary for stdio based MCP) +# Expose port 80 for HTTP server +EXPOSE 80 -# Set environment variable placeholder (user should override these values in production) -ENV YNAB_API_TOKEN="" -# optional: -# ENV YNAB_BUDGET_ID="" - -# Define the command to run your app using node -CMD ["node", "dist/index.js"] +# Run the HTTP server by default on port 80 +CMD ["node", "dist/index.js", "--http", "--port", "80"] diff --git a/README.md b/README.md index f83aec6..3cdd111 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,110 @@ npm install # Build the project npm run build +# Run locally with stdio (default) +npm start + +# Run as HTTP server (for remote hosting) +npm run start:http + +# Run as HTTP server on custom port +node dist/index.js --http --port 8080 +``` + +## Deployment Modes + +### Local Mode (Stdio) +The default mode uses stdio transport for local MCP clients like Claude Desktop: +```bash +npm start +``` + +### HTTP Server Mode +Run as an HTTP server with SSE (Server-Sent Events) transport for remote hosting: +```bash +# Default port 3000 +npm run start:http + +# Custom port +node dist/index.js --http --port 8080 +``` + +When running in HTTP mode, the server exposes: +- `POST /sse` - MCP SSE endpoint for client connections +- `GET /health` - Health check endpoint (returns `{"status": "ok", "version": "0.1.2"}`) +- `POST /message` - Message endpoint (handled by SSE transport) + +Example health check: +```bash +curl http://localhost:3000/health +``` + +#### Connecting MCP Clients to HTTP Server + +The HTTP/SSE mode is designed for MCP clients that support remote server connections. Clients connect to the SSE endpoint: + +``` +POST http://localhost:3000/sse +``` + +> **Note:** Claude Desktop currently only supports local stdio-based MCP servers (spawned via `command`). Use the [Local Development](#local-development) configuration for Claude Desktop. HTTP mode is intended for other MCP clients, web applications, or remote hosting scenarios. + +### Docker Deployment +Run the server in a Docker container with HTTP mode (runs on port 80 by default): + +```bash +# Build the Docker image (builds for linux/amd64) +docker build -t ynab-mcp-server . + +# Run the container on port 80 +docker run -d \ + -p 80:80 \ + -e YNAB_API_TOKEN=your_token_here \ + -e YNAB_BUDGET_ID=your_budget_id \ + --name ynab-mcp \ + ynab-mcp-server + +# Check health +curl http://localhost/health + +# View logs +docker logs ynab-mcp + +# Stop and remove +docker stop ynab-mcp +docker rm ynab-mcp +``` + +Map to different host port (e.g., 3000): +```bash +docker run -d \ + -p 3000:80 \ + -e YNAB_API_TOKEN=your_token_here \ + --name ynab-mcp \ + ynab-mcp-server + +# Access on port 3000 +curl http://localhost:3000/health +``` + +Custom container port: +```bash +docker run -d \ + -p 8080:8080 \ + -e YNAB_API_TOKEN=your_token_here \ + --name ynab-mcp \ + ynab-mcp-server \ + node dist/index.js --http --port 8080 +``` + +**Multi-platform builds:** +The Dockerfile is configured for linux/amd64 by default. To build for other platforms: +```bash +# Build for multiple platforms using buildx +docker buildx build --platform linux/amd64,linux/arm64 -t ynab-mcp-server . + +# Build for specific platform +docker buildx build --platform linux/arm64 -t ynab-mcp-server . ``` ## Project Structure @@ -176,35 +280,65 @@ npx -y @smithery/cli install @calebl/ynab-mcp-server --client claude ### Local Development -Add this configuration to your Claude Desktop config file: +To set up the MCP server locally with Claude Desktop: -**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` -**Windows**: `%APPDATA%/Claude/claude_desktop_config.json` +**1. Clone and build the project:** +```bash +git clone https://github.com/calebl/ynab-mcp-server.git +cd ynab-mcp-server +npm install +npm run build +``` + +**2. Get your YNAB Personal Access Token:** +- Go to https://app.ynab.com/settings/developer +- Create a new Personal Access Token +- Copy the token (you'll only see it once) + +**3. Add the server to Claude Desktop:** + +Open your Claude Desktop config file: +- **MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json` + +Add the following configuration (replace the placeholders with your values): ```json { "mcpServers": { "ynab-mcp-server": { "command": "node", - "args":["/absolute/path/to/ynab-mcp-server/dist/index.js"] + "args": ["/absolute/path/to/ynab-mcp-server/dist/index.js"], + "env": { + "YNAB_API_TOKEN": "your_ynab_personal_access_token" + } } } } ``` -### After Publishing +**4. Restart Claude Desktop** to load the new MCP server. + +**5. Verify the connection** by asking Claude: "List my YNAB budgets" + +> **Tip:** You can optionally add `"YNAB_BUDGET_ID": "your_default_budget_id"` to the `env` object to set a default budget, so you don't have to specify it with each request. + +### After Publishing (via npx) Add this configuration to your Claude Desktop config file: -**MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` -**Windows**: `%APPDATA%/Claude/claude_desktop_config.json` +- **MacOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json` ```json { "mcpServers": { "ynab-mcp-server": { "command": "npx", - "args": ["ynab-mcp-server"] + "args": ["-y", "ynab-mcp-server"], + "env": { + "YNAB_API_TOKEN": "your_ynab_personal_access_token" + } } } } diff --git a/dist/index.js b/dist/index.js index 88f840a..fde1835 100755 --- a/dist/index.js +++ b/dist/index.js @@ -1,6 +1,10 @@ #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import express from "express"; +import cors from "cors"; +import { Command } from "commander"; import * as ynab from "ynab"; // Import all tools import * as ListBudgetsTool from "./tools/ListBudgetsTool.js"; @@ -19,97 +23,163 @@ import * as ListAccountsTool from "./tools/ListAccountsTool.js"; import * as ListScheduledTransactionsTool from "./tools/ListScheduledTransactionsTool.js"; import * as ImportTransactionsTool from "./tools/ImportTransactionsTool.js"; import * as ListMonthsTool from "./tools/ListMonthsTool.js"; -const server = new McpServer({ - name: "ynab-mcp-server", - version: "0.1.2", -}); +const VERSION = "0.1.2"; // Initialize YNAB API const api = new ynab.API(process.env.YNAB_API_TOKEN || ""); -// Register all tools -server.registerTool(ListBudgetsTool.name, { - title: "List Budgets", - description: ListBudgetsTool.description, - inputSchema: ListBudgetsTool.inputSchema, -}, async (input) => ListBudgetsTool.execute(input, api)); -server.registerTool(GetUnapprovedTransactionsTool.name, { - title: "Get Unapproved Transactions", - description: GetUnapprovedTransactionsTool.description, - inputSchema: GetUnapprovedTransactionsTool.inputSchema, -}, async (input) => GetUnapprovedTransactionsTool.execute(input, api)); -server.registerTool(BudgetSummaryTool.name, { - title: "Budget Summary", - description: BudgetSummaryTool.description, - inputSchema: BudgetSummaryTool.inputSchema, -}, async (input) => BudgetSummaryTool.execute(input, api)); -server.registerTool(CreateTransactionTool.name, { - title: "Create Transaction", - description: CreateTransactionTool.description, - inputSchema: CreateTransactionTool.inputSchema, -}, async (input) => CreateTransactionTool.execute(input, api)); -server.registerTool(ApproveTransactionTool.name, { - title: "Approve Transaction", - description: ApproveTransactionTool.description, - inputSchema: ApproveTransactionTool.inputSchema, -}, async (input) => ApproveTransactionTool.execute(input, api)); -server.registerTool(UpdateCategoryBudgetTool.name, { - title: "Update Category Budget", - description: UpdateCategoryBudgetTool.description, - inputSchema: UpdateCategoryBudgetTool.inputSchema, -}, async (input) => UpdateCategoryBudgetTool.execute(input, api)); -server.registerTool(UpdateTransactionTool.name, { - title: "Update Transaction", - description: UpdateTransactionTool.description, - inputSchema: UpdateTransactionTool.inputSchema, -}, async (input) => UpdateTransactionTool.execute(input, api)); -server.registerTool(BulkApproveTransactionsTool.name, { - title: "Bulk Approve Transactions", - description: BulkApproveTransactionsTool.description, - inputSchema: BulkApproveTransactionsTool.inputSchema, -}, async (input) => BulkApproveTransactionsTool.execute(input, api)); -server.registerTool(ListPayeesTool.name, { - title: "List Payees", - description: ListPayeesTool.description, - inputSchema: ListPayeesTool.inputSchema, -}, async (input) => ListPayeesTool.execute(input, api)); -server.registerTool(GetTransactionsTool.name, { - title: "Get Transactions", - description: GetTransactionsTool.description, - inputSchema: GetTransactionsTool.inputSchema, -}, async (input) => GetTransactionsTool.execute(input, api)); -server.registerTool(DeleteTransactionTool.name, { - title: "Delete Transaction", - description: DeleteTransactionTool.description, - inputSchema: DeleteTransactionTool.inputSchema, -}, async (input) => DeleteTransactionTool.execute(input, api)); -server.registerTool(ListCategoriesTool.name, { - title: "List Categories", - description: ListCategoriesTool.description, - inputSchema: ListCategoriesTool.inputSchema, -}, async (input) => ListCategoriesTool.execute(input, api)); -server.registerTool(ListAccountsTool.name, { - title: "List Accounts", - description: ListAccountsTool.description, - inputSchema: ListAccountsTool.inputSchema, -}, async (input) => ListAccountsTool.execute(input, api)); -server.registerTool(ListScheduledTransactionsTool.name, { - title: "List Scheduled Transactions", - description: ListScheduledTransactionsTool.description, - inputSchema: ListScheduledTransactionsTool.inputSchema, -}, async (input) => ListScheduledTransactionsTool.execute(input, api)); -server.registerTool(ImportTransactionsTool.name, { - title: "Import Transactions", - description: ImportTransactionsTool.description, - inputSchema: ImportTransactionsTool.inputSchema, -}, async (input) => ImportTransactionsTool.execute(input, api)); -server.registerTool(ListMonthsTool.name, { - title: "List Months", - description: ListMonthsTool.description, - inputSchema: ListMonthsTool.inputSchema, -}, async (input) => ListMonthsTool.execute(input, api)); -// Start the server -async function main() { +// Function to register all tools on a server instance +function registerTools(server) { + server.registerTool(ListBudgetsTool.name, { + title: "List Budgets", + description: ListBudgetsTool.description, + inputSchema: ListBudgetsTool.inputSchema, + }, async (input) => ListBudgetsTool.execute(input, api)); + server.registerTool(GetUnapprovedTransactionsTool.name, { + title: "Get Unapproved Transactions", + description: GetUnapprovedTransactionsTool.description, + inputSchema: GetUnapprovedTransactionsTool.inputSchema, + }, async (input) => GetUnapprovedTransactionsTool.execute(input, api)); + server.registerTool(BudgetSummaryTool.name, { + title: "Budget Summary", + description: BudgetSummaryTool.description, + inputSchema: BudgetSummaryTool.inputSchema, + }, async (input) => BudgetSummaryTool.execute(input, api)); + server.registerTool(CreateTransactionTool.name, { + title: "Create Transaction", + description: CreateTransactionTool.description, + inputSchema: CreateTransactionTool.inputSchema, + }, async (input) => CreateTransactionTool.execute(input, api)); + server.registerTool(ApproveTransactionTool.name, { + title: "Approve Transaction", + description: ApproveTransactionTool.description, + inputSchema: ApproveTransactionTool.inputSchema, + }, async (input) => ApproveTransactionTool.execute(input, api)); + server.registerTool(UpdateCategoryBudgetTool.name, { + title: "Update Category Budget", + description: UpdateCategoryBudgetTool.description, + inputSchema: UpdateCategoryBudgetTool.inputSchema, + }, async (input) => UpdateCategoryBudgetTool.execute(input, api)); + server.registerTool(UpdateTransactionTool.name, { + title: "Update Transaction", + description: UpdateTransactionTool.description, + inputSchema: UpdateTransactionTool.inputSchema, + }, async (input) => UpdateTransactionTool.execute(input, api)); + server.registerTool(BulkApproveTransactionsTool.name, { + title: "Bulk Approve Transactions", + description: BulkApproveTransactionsTool.description, + inputSchema: BulkApproveTransactionsTool.inputSchema, + }, async (input) => BulkApproveTransactionsTool.execute(input, api)); + server.registerTool(ListPayeesTool.name, { + title: "List Payees", + description: ListPayeesTool.description, + inputSchema: ListPayeesTool.inputSchema, + }, async (input) => ListPayeesTool.execute(input, api)); + server.registerTool(GetTransactionsTool.name, { + title: "Get Transactions", + description: GetTransactionsTool.description, + inputSchema: GetTransactionsTool.inputSchema, + }, async (input) => GetTransactionsTool.execute(input, api)); + server.registerTool(DeleteTransactionTool.name, { + title: "Delete Transaction", + description: DeleteTransactionTool.description, + inputSchema: DeleteTransactionTool.inputSchema, + }, async (input) => DeleteTransactionTool.execute(input, api)); + server.registerTool(ListCategoriesTool.name, { + title: "List Categories", + description: ListCategoriesTool.description, + inputSchema: ListCategoriesTool.inputSchema, + }, async (input) => ListCategoriesTool.execute(input, api)); + server.registerTool(ListAccountsTool.name, { + title: "List Accounts", + description: ListAccountsTool.description, + inputSchema: ListAccountsTool.inputSchema, + }, async (input) => ListAccountsTool.execute(input, api)); + server.registerTool(ListScheduledTransactionsTool.name, { + title: "List Scheduled Transactions", + description: ListScheduledTransactionsTool.description, + inputSchema: ListScheduledTransactionsTool.inputSchema, + }, async (input) => ListScheduledTransactionsTool.execute(input, api)); + server.registerTool(ImportTransactionsTool.name, { + title: "Import Transactions", + description: ImportTransactionsTool.description, + inputSchema: ImportTransactionsTool.inputSchema, + }, async (input) => ImportTransactionsTool.execute(input, api)); + server.registerTool(ListMonthsTool.name, { + title: "List Months", + description: ListMonthsTool.description, + inputSchema: ListMonthsTool.inputSchema, + }, async (input) => ListMonthsTool.execute(input, api)); +} +// Start server in stdio mode +async function startStdioServer() { + const server = new McpServer({ + name: "ynab-mcp-server", + version: VERSION, + }); + registerTools(server); const transport = new StdioServerTransport(); await server.connect(transport); console.error("YNAB MCP server running on stdio"); } +// Start server in HTTP mode +async function startHttpServer(port) { + const app = express(); + app.use(cors()); + app.use(express.json()); + // Health check endpoint + app.get('/health', (_req, res) => { + res.json({ status: 'ok', version: VERSION }); + }); + // SSE endpoint for MCP + app.post('/sse', async (req, res) => { + console.error("New SSE connection"); + const server = new McpServer({ + name: "ynab-mcp-server", + version: VERSION, + }); + registerTools(server); + const transport = new SSEServerTransport('/message', res); + await server.connect(transport); + // Handle client disconnect + req.on('close', () => { + console.error("SSE connection closed"); + }); + }); + // Message endpoint for client requests + app.post('/message', async (req, res) => { + // This is handled by the SSE transport + res.status(405).json({ error: 'Use SSE endpoint' }); + }); + const httpServer = app.listen(port, () => { + console.error(`YNAB MCP server running on http://localhost:${port}`); + console.error(`SSE endpoint: http://localhost:${port}/sse`); + console.error(`Health check: http://localhost:${port}/health`); + }); + // Graceful shutdown + process.on('SIGTERM', () => { + console.error('SIGTERM received, shutting down gracefully'); + httpServer.close(() => { + console.error('Server closed'); + process.exit(0); + }); + }); +} +// Main entry point with CLI +async function main() { + const program = new Command(); + program + .name('ynab-mcp-server') + .description('YNAB MCP Server - provides AI tools for interacting with YNAB budgets') + .version(VERSION); + program + .option('--http', 'Run as HTTP server with SSE transport') + .option('-p, --port ', 'Port for HTTP server', '3000') + .parse(process.argv); + const options = program.opts(); + if (options.http) { + await startHttpServer(parseInt(options.port)); + } + else { + await startStdioServer(); + } +} main().catch(console.error); diff --git a/package-lock.json b/package-lock.json index 7f4cb10..4766502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,34 @@ { "name": "ynab-mcp-server", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ynab-mcp-server", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "@types/axios": "^0.14.4", "axios": "^1.8.4", "commander": "^14.0.1", + "cors": "^2.8.5", + "express": "^5.2.1", "ynab": "^2.9.0", - "zod": "^3.23.8" + "zod": "^4" }, "bin": { "ynab-mcp-server": "dist/index.js" }, "devDependencies": { "@modelcontextprotocol/inspector": "^0.16.5", + "@smithery/sdk": "^3.0.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", "@types/node": "^20.11.24", + "@types/supertest": "^6.0.3", "@vitest/coverage-v8": "^3.2.4", + "supertest": "^7.1.4", "typescript": "^5.3.3", "vitest": "^3.2.4" } @@ -125,9 +132,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -142,9 +149,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -159,9 +166,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -176,9 +183,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -193,9 +200,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -210,9 +217,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -227,9 +234,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -244,9 +251,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -261,9 +268,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -278,9 +285,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -295,9 +302,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -312,9 +319,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -329,9 +336,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -346,9 +353,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -363,9 +370,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -380,9 +387,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -397,9 +404,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -414,9 +421,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -431,9 +438,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -448,9 +455,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -465,9 +472,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -482,9 +489,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -499,9 +506,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -516,9 +523,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -533,9 +540,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -550,9 +557,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -608,6 +615,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -767,6 +786,16 @@ "mcp-inspector-client": "bin/start.js" } }, + "node_modules/@modelcontextprotocol/inspector-client/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@modelcontextprotocol/inspector-server": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/inspector-server/-/inspector-server-0.16.8.tgz", @@ -786,13 +815,35 @@ "mcp-inspector-server": "build/index.js" } }, + "node_modules/@modelcontextprotocol/inspector-server/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/inspector/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.0.tgz", - "integrity": "sha512-JvKyB6YwS3quM+88JPR0axeRgvdDu3Pv6mdZUy+w4qVkCzGgumb9bXG/TmtDRQv+671yaofVfXSQmFLlWU5qPQ==", + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", + "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -800,15 +851,51 @@ "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk/node_modules/pkce-challenge": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", @@ -818,6 +905,29 @@ "node": ">=16.20.0" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2097,6 +2207,37 @@ "win32" ] }, + "node_modules/@smithery/sdk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@smithery/sdk/-/sdk-3.0.1.tgz", + "integrity": "sha512-ngwD/Tb4sYNxadsYEHe57KtDfJ26av9no9O0KF0lKfJLwiaxcj9XkNWWefXHiJX/48zs1zPW6iO+/ruIJvB7qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.1", + "chalk": "^5.6.2", + "express": "^5.1.0", + "jose": "^6.1.0", + "lodash": "^4.17.21", + "okay-error": "^1.0.3" + }, + "peerDependencies": { + "zod": "^4" + } + }, + "node_modules/@smithery/sdk/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -2135,6 +2276,17 @@ "axios": "*" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -2145,6 +2297,33 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2159,6 +2338,45 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", @@ -2169,6 +2387,65 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -2382,6 +2659,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -2394,6 +2672,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", @@ -2440,6 +2757,13 @@ "node": ">=10" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2469,9 +2793,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2487,23 +2811,27 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2815,6 +3143,16 @@ "node": ">=20" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2902,6 +3240,13 @@ "node": ">=6.6.0" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2937,9 +3282,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3031,6 +3376,17 @@ "dev": true, "license": "MIT" }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -3137,9 +3493,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3150,32 +3506,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -3245,18 +3601,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -3332,8 +3689,32 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3431,6 +3812,24 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3531,9 +3930,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -3612,6 +4011,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.3.tgz", + "integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3620,31 +4029,39 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -3805,6 +4222,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -3816,6 +4242,20 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, "license": "MIT" }, "node_modules/loose-envify": { @@ -3937,6 +4377,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4059,6 +4522,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/okay-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/okay-error/-/okay-error-1.0.3.tgz", + "integrity": "sha512-1GZkj84Uw2STYhwcGhEkgvNXkremOEmTwSgufKm9CcprjwKFuF6md5f1CIvWJgtYlyfR6BbZYnjr6HCfhUuCpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4266,15 +4736,16 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4296,18 +4767,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/react": { @@ -4430,6 +4901,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.48.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz", @@ -4877,9 +5357,9 @@ "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -5009,6 +5489,41 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5063,14 +5578,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5209,15 +5724,19 @@ } }, "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -5254,6 +5773,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -5321,18 +5841,18 @@ } }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -5788,21 +6308,21 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.2.tgz", + "integrity": "sha512-b8L8yn4rIVfiXyHAmnr52/ZEpDumlT0bmxiq3Ws1ybrinhflGpt12Hvv54kYnEsGPRs6o/Ka3/ppA2OWY21IVg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/package.json b/package.json index 53aa730..a044caf 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "ynab-mcp-server", - "version": "0.1.2", + "version": "0.1.3", "description": "ynab-mcp-server MCP server", "author": "Caleb LeNoir ", "type": "module", + "module": "./dist/index.js", "bin": { "ynab-mcp-server": "./dist/index.js" }, @@ -15,23 +16,36 @@ "prepare": "npm run build", "watch": "tsc --watch", "start": "node dist/index.js", + "start:http": "node dist/index.js --http", + "start:http:custom": "node dist/index.js --http --port", "debug": "npm run build && npx @modelcontextprotocol/inspector dist/index.js", "test": "vitest", "test:watch": "vitest watch", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "docker:build": "docker build -t ynab-mcp-server .", + "docker:local": "docker run -d -p 9000:80 -e YNAB_API_TOKEN=$YNAB_API_TOKEN -e YNAB_BUDGET_ID=$YNAB_BUDGET_ID --name ynab-mcp ynab-mcp-server", + "docker:stop": "docker stop ynab-mcp", + "docker:remove": "docker rm ynab-mcp" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "@types/axios": "^0.14.4", "axios": "^1.8.4", "commander": "^14.0.1", + "cors": "^2.8.5", + "express": "^5.2.1", "ynab": "^2.9.0", - "zod": "^3.23.8" + "zod": "^4" }, "devDependencies": { "@modelcontextprotocol/inspector": "^0.16.5", + "@smithery/sdk": "^3.0.1", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", "@types/node": "^20.11.24", + "@types/supertest": "^6.0.3", "@vitest/coverage-v8": "^3.2.4", + "supertest": "^7.1.4", "typescript": "^5.3.3", "vitest": "^3.2.4" } diff --git a/smithery.yaml b/smithery.yaml index b955d94..91e267b 100644 --- a/smithery.yaml +++ b/smithery.yaml @@ -1,12 +1,10 @@ # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml +runtime: "container" startCommand: - type: stdio + type: "http" configSchema: - # JSON Schema defining the configuration options for the MCP. - type: object - required: - - ynabApiToken + type: "object" properties: ynabApiToken: type: string @@ -14,17 +12,7 @@ startCommand: ynabBudgetId: type: string description: Optional budget id for the specific budget you want to interact with - commandFunction: - # A JS function that produces the CLI command based on the given config to start the MCP on stdio. - |- - (config) => ({ - command: 'node', - args: ['dist/index.js'], - env: { - YNAB_API_TOKEN: config.ynabApiToken, - YNAB_BUDGET_ID: config.ynabBudgetId || '' - } - }) - exampleConfig: - ynabApiToken: your-ynab-personal-access-token - ynabBudgetId: your-ynab-budget-id + required: ["ynabApiToken"] +build: + dockerfile: "Dockerfile" + dockerBuildPath: "." diff --git a/src/index.ts b/src/index.ts index cd487b3..6885dce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,10 @@ #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import express from "express"; +import cors from "cors"; +import { Command } from "commander"; import * as ynab from "ynab"; // Import all tools @@ -21,117 +25,199 @@ import * as ListScheduledTransactionsTool from "./tools/ListScheduledTransaction import * as ImportTransactionsTool from "./tools/ImportTransactionsTool.js"; import * as ListMonthsTool from "./tools/ListMonthsTool.js"; -const server = new McpServer({ - name: "ynab-mcp-server", - version: "0.1.2", -}); +const VERSION = "0.1.2"; // Initialize YNAB API const api = new ynab.API(process.env.YNAB_API_TOKEN || ""); -// Register all tools -server.registerTool(ListBudgetsTool.name, { - title: "List Budgets", - description: ListBudgetsTool.description, - inputSchema: ListBudgetsTool.inputSchema, -}, async (input) => ListBudgetsTool.execute(input, api)); - -server.registerTool(GetUnapprovedTransactionsTool.name, { - title: "Get Unapproved Transactions", - description: GetUnapprovedTransactionsTool.description, - inputSchema: GetUnapprovedTransactionsTool.inputSchema, -}, async (input) => GetUnapprovedTransactionsTool.execute(input, api)); - -server.registerTool(BudgetSummaryTool.name, { - title: "Budget Summary", - description: BudgetSummaryTool.description, - inputSchema: BudgetSummaryTool.inputSchema, -}, async (input) => BudgetSummaryTool.execute(input, api)); - -server.registerTool(CreateTransactionTool.name, { - title: "Create Transaction", - description: CreateTransactionTool.description, - inputSchema: CreateTransactionTool.inputSchema, -}, async (input) => CreateTransactionTool.execute(input, api)); - -server.registerTool(ApproveTransactionTool.name, { - title: "Approve Transaction", - description: ApproveTransactionTool.description, - inputSchema: ApproveTransactionTool.inputSchema, -}, async (input) => ApproveTransactionTool.execute(input, api)); - -server.registerTool(UpdateCategoryBudgetTool.name, { - title: "Update Category Budget", - description: UpdateCategoryBudgetTool.description, - inputSchema: UpdateCategoryBudgetTool.inputSchema, -}, async (input) => UpdateCategoryBudgetTool.execute(input, api)); - -server.registerTool(UpdateTransactionTool.name, { - title: "Update Transaction", - description: UpdateTransactionTool.description, - inputSchema: UpdateTransactionTool.inputSchema, -}, async (input) => UpdateTransactionTool.execute(input, api)); - -server.registerTool(BulkApproveTransactionsTool.name, { - title: "Bulk Approve Transactions", - description: BulkApproveTransactionsTool.description, - inputSchema: BulkApproveTransactionsTool.inputSchema, -}, async (input) => BulkApproveTransactionsTool.execute(input, api)); - -server.registerTool(ListPayeesTool.name, { - title: "List Payees", - description: ListPayeesTool.description, - inputSchema: ListPayeesTool.inputSchema, -}, async (input) => ListPayeesTool.execute(input, api)); - -server.registerTool(GetTransactionsTool.name, { - title: "Get Transactions", - description: GetTransactionsTool.description, - inputSchema: GetTransactionsTool.inputSchema, -}, async (input) => GetTransactionsTool.execute(input, api)); - -server.registerTool(DeleteTransactionTool.name, { - title: "Delete Transaction", - description: DeleteTransactionTool.description, - inputSchema: DeleteTransactionTool.inputSchema, -}, async (input) => DeleteTransactionTool.execute(input, api)); - -server.registerTool(ListCategoriesTool.name, { - title: "List Categories", - description: ListCategoriesTool.description, - inputSchema: ListCategoriesTool.inputSchema, -}, async (input) => ListCategoriesTool.execute(input, api)); - -server.registerTool(ListAccountsTool.name, { - title: "List Accounts", - description: ListAccountsTool.description, - inputSchema: ListAccountsTool.inputSchema, -}, async (input) => ListAccountsTool.execute(input, api)); - -server.registerTool(ListScheduledTransactionsTool.name, { - title: "List Scheduled Transactions", - description: ListScheduledTransactionsTool.description, - inputSchema: ListScheduledTransactionsTool.inputSchema, -}, async (input) => ListScheduledTransactionsTool.execute(input, api)); - -server.registerTool(ImportTransactionsTool.name, { - title: "Import Transactions", - description: ImportTransactionsTool.description, - inputSchema: ImportTransactionsTool.inputSchema, -}, async (input) => ImportTransactionsTool.execute(input, api)); - -server.registerTool(ListMonthsTool.name, { - title: "List Months", - description: ListMonthsTool.description, - inputSchema: ListMonthsTool.inputSchema, -}, async (input) => ListMonthsTool.execute(input, api)); - -// Start the server -async function main() { +// Function to register all tools on a server instance +function registerTools(server: McpServer) { + server.registerTool(ListBudgetsTool.name, { + title: "List Budgets", + description: ListBudgetsTool.description, + inputSchema: ListBudgetsTool.inputSchema, + }, async (input) => ListBudgetsTool.execute(input, api)); + + server.registerTool(GetUnapprovedTransactionsTool.name, { + title: "Get Unapproved Transactions", + description: GetUnapprovedTransactionsTool.description, + inputSchema: GetUnapprovedTransactionsTool.inputSchema, + }, async (input) => GetUnapprovedTransactionsTool.execute(input, api)); + + server.registerTool(BudgetSummaryTool.name, { + title: "Budget Summary", + description: BudgetSummaryTool.description, + inputSchema: BudgetSummaryTool.inputSchema, + }, async (input) => BudgetSummaryTool.execute(input, api)); + + server.registerTool(CreateTransactionTool.name, { + title: "Create Transaction", + description: CreateTransactionTool.description, + inputSchema: CreateTransactionTool.inputSchema, + }, async (input) => CreateTransactionTool.execute(input, api)); + + server.registerTool(ApproveTransactionTool.name, { + title: "Approve Transaction", + description: ApproveTransactionTool.description, + inputSchema: ApproveTransactionTool.inputSchema, + }, async (input) => ApproveTransactionTool.execute(input, api)); + + server.registerTool(UpdateCategoryBudgetTool.name, { + title: "Update Category Budget", + description: UpdateCategoryBudgetTool.description, + inputSchema: UpdateCategoryBudgetTool.inputSchema, + }, async (input) => UpdateCategoryBudgetTool.execute(input, api)); + + server.registerTool(UpdateTransactionTool.name, { + title: "Update Transaction", + description: UpdateTransactionTool.description, + inputSchema: UpdateTransactionTool.inputSchema, + }, async (input) => UpdateTransactionTool.execute(input, api)); + + server.registerTool(BulkApproveTransactionsTool.name, { + title: "Bulk Approve Transactions", + description: BulkApproveTransactionsTool.description, + inputSchema: BulkApproveTransactionsTool.inputSchema, + }, async (input) => BulkApproveTransactionsTool.execute(input, api)); + + server.registerTool(ListPayeesTool.name, { + title: "List Payees", + description: ListPayeesTool.description, + inputSchema: ListPayeesTool.inputSchema, + }, async (input) => ListPayeesTool.execute(input, api)); + + server.registerTool(GetTransactionsTool.name, { + title: "Get Transactions", + description: GetTransactionsTool.description, + inputSchema: GetTransactionsTool.inputSchema, + }, async (input) => GetTransactionsTool.execute(input, api)); + + server.registerTool(DeleteTransactionTool.name, { + title: "Delete Transaction", + description: DeleteTransactionTool.description, + inputSchema: DeleteTransactionTool.inputSchema, + }, async (input) => DeleteTransactionTool.execute(input, api)); + + server.registerTool(ListCategoriesTool.name, { + title: "List Categories", + description: ListCategoriesTool.description, + inputSchema: ListCategoriesTool.inputSchema, + }, async (input) => ListCategoriesTool.execute(input, api)); + + server.registerTool(ListAccountsTool.name, { + title: "List Accounts", + description: ListAccountsTool.description, + inputSchema: ListAccountsTool.inputSchema, + }, async (input) => ListAccountsTool.execute(input, api)); + + server.registerTool(ListScheduledTransactionsTool.name, { + title: "List Scheduled Transactions", + description: ListScheduledTransactionsTool.description, + inputSchema: ListScheduledTransactionsTool.inputSchema, + }, async (input) => ListScheduledTransactionsTool.execute(input, api)); + + server.registerTool(ImportTransactionsTool.name, { + title: "Import Transactions", + description: ImportTransactionsTool.description, + inputSchema: ImportTransactionsTool.inputSchema, + }, async (input) => ImportTransactionsTool.execute(input, api)); + + server.registerTool(ListMonthsTool.name, { + title: "List Months", + description: ListMonthsTool.description, + inputSchema: ListMonthsTool.inputSchema, + }, async (input) => ListMonthsTool.execute(input, api)); +} + +// Start server in stdio mode +async function startStdioServer() { + const server = new McpServer({ + name: "ynab-mcp-server", + version: VERSION, + }); + + registerTools(server); + const transport = new StdioServerTransport(); await server.connect(transport); console.error("YNAB MCP server running on stdio"); } +// Start server in HTTP mode +async function startHttpServer(port: number) { + const app = express(); + app.use(cors()); + app.use(express.json()); + + // Health check endpoint + app.get('/health', (_req, res) => { + res.json({ status: 'ok', version: VERSION }); + }); + + // SSE endpoint for MCP + app.post('/sse', async (req, res) => { + console.error("New SSE connection"); + + const server = new McpServer({ + name: "ynab-mcp-server", + version: VERSION, + }); + + registerTools(server); + + const transport = new SSEServerTransport('/message', res); + await server.connect(transport); + + // Handle client disconnect + req.on('close', () => { + console.error("SSE connection closed"); + }); + }); + + // Message endpoint for client requests + app.post('/message', async (req, res) => { + // This is handled by the SSE transport + res.status(405).json({ error: 'Use SSE endpoint' }); + }); + + const httpServer = app.listen(port, () => { + console.error(`YNAB MCP server running on http://localhost:${port}`); + console.error(`SSE endpoint: http://localhost:${port}/sse`); + console.error(`Health check: http://localhost:${port}/health`); + }); + + // Graceful shutdown + process.on('SIGTERM', () => { + console.error('SIGTERM received, shutting down gracefully'); + httpServer.close(() => { + console.error('Server closed'); + process.exit(0); + }); + }); +} + +// Main entry point with CLI +async function main() { + const program = new Command(); + + program + .name('ynab-mcp-server') + .description('YNAB MCP Server - provides AI tools for interacting with YNAB budgets') + .version(VERSION); + + program + .option('--http', 'Run as HTTP server with SSE transport') + .option('-p, --port ', 'Port for HTTP server', '3000') + .parse(process.argv); + + const options = program.opts(); + + if (options.http) { + await startHttpServer(parseInt(options.port)); + } else { + await startStdioServer(); + } +} + main().catch(console.error); diff --git a/src/tests/cli.test.ts b/src/tests/cli.test.ts new file mode 100644 index 0000000..2fd5bac --- /dev/null +++ b/src/tests/cli.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { Command } from 'commander'; + +describe('CLI Arguments', () => { + let program: Command; + const VERSION = '0.1.2'; + + beforeEach(() => { + program = new Command(); + program + .name('ynab-mcp-server') + .description('YNAB MCP Server - provides AI tools for interacting with YNAB budgets') + .version(VERSION); + + program + .option('--http', 'Run as HTTP server with SSE transport') + .option('-p, --port ', 'Port for HTTP server', '3000'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Mode Selection', () => { + it('should default to stdio mode when no arguments provided', () => { + program.parse(['node', 'index.js']); + const options = program.opts(); + + expect(options.http).toBeUndefined(); + }); + + it('should enable HTTP mode with --http flag', () => { + program.parse(['node', 'index.js', '--http']); + const options = program.opts(); + + expect(options.http).toBe(true); + }); + + it('should parse multiple arguments together', () => { + program.parse(['node', 'index.js', '--http', '--port', '8080']); + const options = program.opts(); + + expect(options.http).toBe(true); + expect(options.port).toBe('8080'); + }); + + it('should handle stdio mode without explicit flag', () => { + program.parse(['node', 'index.js']); + const options = program.opts(); + + expect(options.http).toBeFalsy(); + }); + }); + + describe('Port Configuration', () => { + it('should set custom port with --port flag', () => { + program.parse(['node', 'index.js', '--http', '--port', '8080']); + const options = program.opts(); + + expect(options.port).toBe('8080'); + }); + + it('should set custom port with -p short flag', () => { + program.parse(['node', 'index.js', '--http', '-p', '8080']); + const options = program.opts(); + + expect(options.port).toBe('8080'); + }); + + it('should default to port 3000 in HTTP mode', () => { + program.parse(['node', 'index.js', '--http']); + const options = program.opts(); + + expect(options.port).toBe('3000'); + }); + + it('should accept valid port numbers', () => { + const validPorts = ['80', '443', '3000', '8080', '65535']; + + validPorts.forEach(port => { + const testProgram = new Command(); + testProgram + .name('ynab-mcp-server') + .version(VERSION) + .option('--http', 'Run as HTTP server') + .option('-p, --port ', 'Port for HTTP server', '3000'); + + testProgram.parse(['node', 'index.js', '--http', '--port', port]); + const options = testProgram.opts(); + + expect(options.port).toBe(port); + }); + }); + + it('should handle low port numbers', () => { + program.parse(['node', 'index.js', '--http', '--port', '80']); + const options = program.opts(); + + expect(options.port).toBe('80'); + }); + + it('should handle high port numbers', () => { + program.parse(['node', 'index.js', '--http', '--port', '65535']); + const options = program.opts(); + + expect(options.port).toBe('65535'); + }); + + it('should parse port as string (requires conversion to number)', () => { + program.parse(['node', 'index.js', '--http', '--port', '8080']); + const options = program.opts(); + + expect(typeof options.port).toBe('string'); + expect(parseInt(options.port)).toBe(8080); + }); + }); + + describe('Version and Help', () => { + it('should have correct version', () => { + expect(program.version()).toBe(VERSION); + }); + + it('should have correct name', () => { + expect(program.name()).toBe('ynab-mcp-server'); + }); + + it('should have description', () => { + const description = program.description(); + expect(description).toContain('YNAB'); + expect(description).toContain('MCP'); + }); + + it('should register --http option', () => { + const httpOption = program.options.find(opt => opt.long === '--http'); + expect(httpOption).toBeDefined(); + expect(httpOption?.description).toContain('HTTP'); + }); + + it('should register --port option', () => { + const portOption = program.options.find(opt => opt.long === '--port'); + expect(portOption).toBeDefined(); + expect(portOption?.description).toContain('Port'); + }); + + it('should have -p as short flag for port', () => { + const portOption = program.options.find(opt => opt.short === '-p'); + expect(portOption).toBeDefined(); + expect(portOption?.long).toBe('--port'); + }); + }); + + describe('Argument Combinations', () => { + it('should handle only port flag without http flag', () => { + program.parse(['node', 'index.js', '--port', '8080']); + const options = program.opts(); + + expect(options.http).toBeUndefined(); + expect(options.port).toBe('8080'); + }); + + it('should handle flags in different order', () => { + program.parse(['node', 'index.js', '--port', '8080', '--http']); + const options = program.opts(); + + expect(options.http).toBe(true); + expect(options.port).toBe('8080'); + }); + + it('should handle short and long flags together', () => { + program.parse(['node', 'index.js', '--http', '-p', '9000']); + const options = program.opts(); + + expect(options.http).toBe(true); + expect(options.port).toBe('9000'); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty port value gracefully', () => { + // Commander will use default value if port is not provided + program.parse(['node', 'index.js', '--http']); + const options = program.opts(); + + expect(options.port).toBe('3000'); + }); + + it('should parse arguments correctly with equals sign', () => { + program.parse(['node', 'index.js', '--http', '--port=8080']); + const options = program.opts(); + + expect(options.port).toBe('8080'); + }); + + it('should handle no arguments', () => { + program.parse(['node', 'index.js']); + const options = program.opts(); + + expect(options.http).toBeUndefined(); + expect(options.port).toBe('3000'); // default value + }); + }); +}); diff --git a/src/tests/registerTools.test.ts b/src/tests/registerTools.test.ts new file mode 100644 index 0000000..74eb3b0 --- /dev/null +++ b/src/tests/registerTools.test.ts @@ -0,0 +1,303 @@ +import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +// Import all tools +import * as ListBudgetsTool from '../tools/ListBudgetsTool.js'; +import * as GetUnapprovedTransactionsTool from '../tools/GetUnapprovedTransactionsTool.js'; +import * as BudgetSummaryTool from '../tools/BudgetSummaryTool.js'; +import * as CreateTransactionTool from '../tools/CreateTransactionTool.js'; +import * as ApproveTransactionTool from '../tools/ApproveTransactionTool.js'; +import * as UpdateCategoryBudgetTool from '../tools/UpdateCategoryBudgetTool.js'; +import * as UpdateTransactionTool from '../tools/UpdateTransactionTool.js'; +import * as BulkApproveTransactionsTool from '../tools/BulkApproveTransactionsTool.js'; +import * as ListPayeesTool from '../tools/ListPayeesTool.js'; +import * as GetTransactionsTool from '../tools/GetTransactionsTool.js'; +import * as DeleteTransactionTool from '../tools/DeleteTransactionTool.js'; +import * as ListCategoriesTool from '../tools/ListCategoriesTool.js'; +import * as ListAccountsTool from '../tools/ListAccountsTool.js'; +import * as ListScheduledTransactionsTool from '../tools/ListScheduledTransactionsTool.js'; +import * as ImportTransactionsTool from '../tools/ImportTransactionsTool.js'; +import * as ListMonthsTool from '../tools/ListMonthsTool.js'; + +vi.mock('@modelcontextprotocol/sdk/server/mcp.js'); + +describe('Tool Registration', () => { + let mockServer: { + registerTool: Mock; + }; + + beforeEach(() => { + vi.clearAllMocks(); + + mockServer = { + registerTool: vi.fn(), + }; + + (McpServer as any).mockImplementation(() => mockServer); + }); + + // Expected tool list + const expectedTools = [ + { tool: ListBudgetsTool, title: 'List Budgets' }, + { tool: GetUnapprovedTransactionsTool, title: 'Get Unapproved Transactions' }, + { tool: BudgetSummaryTool, title: 'Budget Summary' }, + { tool: CreateTransactionTool, title: 'Create Transaction' }, + { tool: ApproveTransactionTool, title: 'Approve Transaction' }, + { tool: UpdateCategoryBudgetTool, title: 'Update Category Budget' }, + { tool: UpdateTransactionTool, title: 'Update Transaction' }, + { tool: BulkApproveTransactionsTool, title: 'Bulk Approve Transactions' }, + { tool: ListPayeesTool, title: 'List Payees' }, + { tool: GetTransactionsTool, title: 'Get Transactions' }, + { tool: DeleteTransactionTool, title: 'Delete Transaction' }, + { tool: ListCategoriesTool, title: 'List Categories' }, + { tool: ListAccountsTool, title: 'List Accounts' }, + { tool: ListScheduledTransactionsTool, title: 'List Scheduled Transactions' }, + { tool: ImportTransactionsTool, title: 'Import Transactions' }, + { tool: ListMonthsTool, title: 'List Months' }, + ]; + + describe('Tool Exports', () => { + it('should have correct number of tools', () => { + expect(expectedTools.length).toBe(16); + }); + + expectedTools.forEach(({ tool, title }) => { + describe(`${title}`, () => { + it('should export name', () => { + expect(tool.name).toBeDefined(); + expect(typeof tool.name).toBe('string'); + expect(tool.name.length).toBeGreaterThan(0); + }); + + it('should export description', () => { + expect(tool.description).toBeDefined(); + expect(typeof tool.description).toBe('string'); + expect(tool.description.length).toBeGreaterThan(0); + }); + + it('should export inputSchema', () => { + expect(tool.inputSchema).toBeDefined(); + expect(typeof tool.inputSchema).toBe('object'); + }); + + it('should export execute function', () => { + expect(tool.execute).toBeDefined(); + expect(typeof tool.execute).toBe('function'); + }); + + it('should have async execute function', () => { + const result = tool.execute({}, {} as any); + expect(result).toBeInstanceOf(Promise); + }); + + it('should have name in snake_case format', () => { + expect(tool.name).toMatch(/^[a-z_]+$/); + }); + + it('should have name starting with ynab_', () => { + expect(tool.name).toMatch(/^ynab_/); + }); + }); + }); + }); + + describe('Tool Names', () => { + it('should have unique tool names', () => { + const names = expectedTools.map(({ tool }) => tool.name); + const uniqueNames = new Set(names); + + expect(uniqueNames.size).toBe(names.length); + }); + + it('should have expected tool names', () => { + const expectedNames = [ + 'ynab_list_budgets', + 'ynab_get_unapproved_transactions', + 'ynab_budget_summary', + 'ynab_create_transaction', + 'ynab_approve_transaction', + 'ynab_update_category_budget', + 'ynab_update_transaction', + 'ynab_bulk_approve_transactions', + 'ynab_list_payees', + 'ynab_get_transactions', + 'ynab_delete_transaction', + 'ynab_list_categories', + 'ynab_list_accounts', + 'ynab_list_scheduled_transactions', + 'ynab_import_transactions', + 'ynab_list_months', + ]; + + const actualNames = expectedTools.map(({ tool }) => tool.name); + + expectedNames.forEach(name => { + expect(actualNames).toContain(name); + }); + }); + }); + + describe('Tool Descriptions', () => { + it('should have non-empty descriptions', () => { + expectedTools.forEach(({ tool, title }) => { + expect(tool.description.length).toBeGreaterThan(0); + }); + }); + + it('should have meaningful descriptions', () => { + expectedTools.forEach(({ tool, title }) => { + // Description should be at least 10 characters + expect(tool.description.length).toBeGreaterThanOrEqual(10); + }); + }); + }); + + describe('Input Schemas', () => { + it('should have valid input schemas', () => { + expectedTools.forEach(({ tool, title }) => { + expect(tool.inputSchema).toBeDefined(); + expect(typeof tool.inputSchema).toBe('object'); + }); + }); + + it('should handle tools with empty schemas', () => { + // ListBudgetsTool has an empty schema + expect(ListBudgetsTool.inputSchema).toEqual({}); + }); + + it('should handle tools with complex schemas', () => { + // CreateTransactionTool has a complex schema + expect(Object.keys(CreateTransactionTool.inputSchema).length).toBeGreaterThan(0); + }); + }); + + describe('Execute Functions', () => { + it('should return promises', () => { + expectedTools.forEach(({ tool, title }) => { + const result = tool.execute({}, {} as any); + expect(result).toBeInstanceOf(Promise); + }); + }); + + it('should accept input parameter', () => { + expectedTools.forEach(({ tool, title }) => { + const executeParams = tool.execute.length; + expect(executeParams).toBeGreaterThanOrEqual(1); + }); + }); + + it('should accept API parameter', () => { + expectedTools.forEach(({ tool, title }) => { + const executeParams = tool.execute.length; + expect(executeParams).toBe(2); + }); + }); + }); + + describe('Tool Registration Process', () => { + it('should call registerTool for each tool', () => { + // Simulate the registerTools function + const server = new McpServer({ name: 'test', version: '1.0.0' }) as any; + + expectedTools.forEach(({ tool, title }) => { + server.registerTool( + tool.name, + { + title: title, + description: tool.description, + inputSchema: tool.inputSchema, + }, + async (input: any) => tool.execute(input, {} as any) + ); + }); + + expect(mockServer.registerTool).toHaveBeenCalledTimes(16); + }); + + it('should register tools with correct parameters', () => { + const server = new McpServer({ name: 'test', version: '1.0.0' }) as any; + + server.registerTool( + ListBudgetsTool.name, + { + title: 'List Budgets', + description: ListBudgetsTool.description, + inputSchema: ListBudgetsTool.inputSchema, + }, + async (input: any) => ListBudgetsTool.execute(input, {} as any) + ); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + ListBudgetsTool.name, + expect.objectContaining({ + title: 'List Budgets', + description: ListBudgetsTool.description, + inputSchema: ListBudgetsTool.inputSchema, + }), + expect.any(Function) + ); + }); + + it('should register tools with async handlers', () => { + const server = new McpServer({ name: 'test', version: '1.0.0' }) as any; + + const handler = async (input: any) => ListBudgetsTool.execute(input, {} as any); + + server.registerTool( + ListBudgetsTool.name, + { + title: 'List Budgets', + description: ListBudgetsTool.description, + inputSchema: ListBudgetsTool.inputSchema, + }, + handler + ); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + ListBudgetsTool.name, + expect.any(Object), + expect.any(Function) + ); + + // Verify handler is async + const registeredHandler = mockServer.registerTool.mock.calls[0][2]; + expect(registeredHandler({}, {} as any)).toBeInstanceOf(Promise); + }); + }); + + describe('Tool Count Verification', () => { + it('should match expected tool count', () => { + expect(expectedTools.length).toBe(16); + }); + + it('should have all tools imported', () => { + const toolModules = [ + ListBudgetsTool, + GetUnapprovedTransactionsTool, + BudgetSummaryTool, + CreateTransactionTool, + ApproveTransactionTool, + UpdateCategoryBudgetTool, + UpdateTransactionTool, + BulkApproveTransactionsTool, + ListPayeesTool, + GetTransactionsTool, + DeleteTransactionTool, + ListCategoriesTool, + ListAccountsTool, + ListScheduledTransactionsTool, + ImportTransactionsTool, + ListMonthsTool, + ]; + + expect(toolModules.length).toBe(16); + + toolModules.forEach(tool => { + expect(tool.name).toBeDefined(); + expect(tool.description).toBeDefined(); + expect(tool.inputSchema).toBeDefined(); + expect(tool.execute).toBeDefined(); + }); + }); + }); +}); diff --git a/src/tests/server.test.ts b/src/tests/server.test.ts new file mode 100644 index 0000000..f2e9366 --- /dev/null +++ b/src/tests/server.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import request from 'supertest'; +import express, { Express } from 'express'; +import cors from 'cors'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +// Mock the MCP SDK +vi.mock('@modelcontextprotocol/sdk/server/mcp.js'); +vi.mock('@modelcontextprotocol/sdk/server/sse.js'); + +describe('HTTP Server', () => { + let app: Express; + const VERSION = '0.1.2'; + + beforeEach(() => { + vi.clearAllMocks(); + + // Create a basic Express app similar to the actual server + app = express(); + app.use(cors()); + app.use(express.json()); + + // Health check endpoint + app.get('/health', (_req, res) => { + res.json({ status: 'ok', version: VERSION }); + }); + + // Message endpoint + app.post('/message', async (_req, res) => { + res.status(405).json({ error: 'Use SSE endpoint' }); + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Server Configuration', () => { + it('should have CORS middleware enabled', async () => { + const response = await request(app) + .options('/health') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET'); + + expect(response.headers['access-control-allow-origin']).toBeDefined(); + }); + + it('should have JSON middleware enabled', async () => { + const response = await request(app) + .post('/message') + .send({ test: 'data' }) + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(405); + expect(response.body).toEqual({ error: 'Use SSE endpoint' }); + }); + }); + + describe('Health Endpoint', () => { + it('should return 200 status', async () => { + const response = await request(app).get('/health'); + expect(response.status).toBe(200); + }); + + it('should return correct JSON structure', async () => { + const response = await request(app).get('/health'); + + expect(response.body).toHaveProperty('status'); + expect(response.body).toHaveProperty('version'); + }); + + it('should include correct version number', async () => { + const response = await request(app).get('/health'); + + expect(response.body.version).toBe(VERSION); + }); + + it('should return ok status', async () => { + const response = await request(app).get('/health'); + + expect(response.body.status).toBe('ok'); + }); + + it('should return application/json content type', async () => { + const response = await request(app).get('/health'); + + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + + it('should respond quickly (under 100ms)', async () => { + const start = Date.now(); + await request(app).get('/health'); + const duration = Date.now() - start; + + expect(duration).toBeLessThan(100); + }); + + it('should handle multiple concurrent requests', async () => { + const requests = Array(10).fill(null).map(() => + request(app).get('/health') + ); + + const responses = await Promise.all(requests); + + responses.forEach(response => { + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + }); + }); + }); + + describe('Message Endpoint', () => { + it('should return 405 status', async () => { + const response = await request(app).post('/message'); + + expect(response.status).toBe(405); + }); + + it('should return appropriate error message', async () => { + const response = await request(app).post('/message'); + + expect(response.body.error).toBe('Use SSE endpoint'); + }); + + it('should return JSON response', async () => { + const response = await request(app).post('/message'); + + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + }); + + describe('Error Handling', () => { + it('should handle invalid routes with 404', async () => { + const response = await request(app).get('/invalid-route'); + + expect(response.status).toBe(404); + }); + + it('should handle invalid JSON gracefully', async () => { + const response = await request(app) + .post('/message') + .send('invalid json') + .set('Content-Type', 'application/json'); + + // Express will either parse it or return 400, both are acceptable + expect([400, 405]).toContain(response.status); + }); + }); + + describe('CORS Configuration', () => { + it('should allow cross-origin requests', async () => { + const response = await request(app) + .get('/health') + .set('Origin', 'http://example.com'); + + expect(response.headers['access-control-allow-origin']).toBeDefined(); + }); + + it('should support preflight requests', async () => { + const response = await request(app) + .options('/health') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'GET'); + + expect(response.status).toBe(204); + }); + }); +}); diff --git a/src/tests/sse.test.ts b/src/tests/sse.test.ts new file mode 100644 index 0000000..c75e1ad --- /dev/null +++ b/src/tests/sse.test.ts @@ -0,0 +1,353 @@ +import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest'; +import request from 'supertest'; +import express, { Express } from 'express'; +import cors from 'cors'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; + +// Mock the MCP SDK +vi.mock('@modelcontextprotocol/sdk/server/mcp.js'); +vi.mock('@modelcontextprotocol/sdk/server/sse.js'); + +describe('SSE Endpoint', () => { + let app: Express; + let mockServer: { + connect: Mock; + registerTool: Mock; + }; + let mockTransport: any; + const VERSION = '0.1.2'; + + beforeEach(() => { + vi.clearAllMocks(); + + mockServer = { + connect: vi.fn().mockResolvedValue(undefined), + registerTool: vi.fn(), + }; + + mockTransport = { + start: vi.fn(), + stop: vi.fn(), + }; + + (McpServer as any).mockImplementation(() => mockServer); + (SSEServerTransport as any).mockImplementation(() => mockTransport); + + // Create Express app with SSE endpoint + app = express(); + app.use(cors()); + app.use(express.json()); + + // SSE endpoint for MCP + app.post('/sse', async (req, res) => { + const server = new McpServer({ + name: 'ynab-mcp-server', + version: VERSION, + }); + + const transport = new SSEServerTransport('/message', res); + await server.connect(transport); + + req.on('close', () => { + // Connection closed + }); + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Connection Handling', () => { + it('should accept POST requests to /sse', async () => { + // SSE connections stay open, so we catch the timeout and verify mocks were called + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + // Verify the endpoint was hit and server was created + expect(McpServer).toHaveBeenCalled(); + }); + + it('should create new McpServer instance per connection', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(McpServer).toHaveBeenCalledWith({ + name: 'ynab-mcp-server', + version: VERSION, + }); + }); + + it('should initialize SSEServerTransport', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(SSEServerTransport).toHaveBeenCalledWith( + '/message', + expect.any(Object) + ); + }); + + it('should connect server to transport', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(mockServer.connect).toHaveBeenCalledWith(mockTransport); + }); + + it('should handle connection close event', async () => { + const closeHandler = vi.fn(); + + const customApp = express(); + customApp.use(cors()); + customApp.use(express.json()); + + customApp.post('/sse', async (req, res) => { + const server = new McpServer({ + name: 'ynab-mcp-server', + version: VERSION, + }); + + const transport = new SSEServerTransport('/message', res); + await server.connect(transport); + + req.on('close', closeHandler); + }); + + const agent = request(customApp).post('/sse'); + + // Start the request but don't wait for completion + const promise = agent.timeout(500).catch(() => {}); + + // Give it time to set up handlers + await new Promise(resolve => setTimeout(resolve, 100)); + + await promise; + + // The close handler should be registered + // Note: We can't easily trigger the close event in tests + }); + }); + + describe('Multiple Connections', () => { + it('should create separate server instances for concurrent connections', async () => { + const requests = [ + request(app).post('/sse').timeout(500).catch(() => {}), + request(app).post('/sse').timeout(500).catch(() => {}), + ]; + + await Promise.all(requests); + + // Should create multiple server instances + expect(McpServer).toHaveBeenCalledTimes(2); + }); + + it('should create separate transport instances for concurrent connections', async () => { + const requests = [ + request(app).post('/sse').timeout(500).catch(() => {}), + request(app).post('/sse').timeout(500).catch(() => {}), + ]; + + await Promise.all(requests); + + // Should create multiple transport instances + expect(SSEServerTransport).toHaveBeenCalledTimes(2); + }); + + it('should handle multiple connections independently', async () => { + const request1 = request(app).post('/sse').timeout(500).catch(() => {}); + const request2 = request(app).post('/sse').timeout(500).catch(() => {}); + + await Promise.all([request1, request2]); + + // Each connection should get its own server and transport + expect(McpServer).toHaveBeenCalledTimes(2); + expect(SSEServerTransport).toHaveBeenCalledTimes(2); + expect(mockServer.connect).toHaveBeenCalledTimes(2); + }); + }); + + describe('Transport Configuration', () => { + it('should pass correct endpoint to SSEServerTransport', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(SSEServerTransport).toHaveBeenCalledWith( + '/message', + expect.any(Object) + ); + }); + + it('should pass response object to SSEServerTransport', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + const transportArgs = (SSEServerTransport as any).mock.calls[0]; + expect(transportArgs[1]).toBeDefined(); + expect(typeof transportArgs[1]).toBe('object'); + }); + + it('should use /message as message endpoint', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(SSEServerTransport).toHaveBeenCalledWith( + '/message', + expect.any(Object) + ); + }); + }); + + describe('Server Configuration', () => { + it('should create server with correct name', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(McpServer).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'ynab-mcp-server', + }) + ); + }); + + it('should create server with correct version', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(McpServer).toHaveBeenCalledWith( + expect.objectContaining({ + version: VERSION, + }) + ); + }); + + it('should create server with both name and version', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(McpServer).toHaveBeenCalledWith({ + name: 'ynab-mcp-server', + version: VERSION, + }); + }); + }); + + describe('Error Handling', () => { + it('should handle server connection errors', async () => { + mockServer.connect.mockRejectedValue(new Error('Connection failed')); + + try { + await request(app) + .post('/sse') + .timeout(1000); + } catch (error) { + // Expected to fail or timeout + } + + expect(mockServer.connect).toHaveBeenCalled(); + }); + + it('should handle transport initialization errors', async () => { + (SSEServerTransport as any).mockImplementation(() => { + throw new Error('Transport init failed'); + }); + + try { + await request(app) + .post('/sse') + .timeout(1000); + } catch (error) { + // Expected to fail + } + + expect(SSEServerTransport).toHaveBeenCalled(); + }); + + it('should reject non-POST requests', async () => { + const response = await request(app).get('/sse'); + + expect(response.status).toBe(404); + }); + }); + + describe('Request Headers', () => { + it('should accept requests with JSON content type', async () => { + await request(app) + .post('/sse') + .set('Content-Type', 'application/json') + .timeout(500) + .catch(() => {}); + + expect(McpServer).toHaveBeenCalled(); + }); + + it('should accept requests without content type', async () => { + await request(app) + .post('/sse') + .timeout(500) + .catch(() => {}); + + expect(McpServer).toHaveBeenCalled(); + }); + + it('should handle CORS preflight requests', async () => { + const response = await request(app) + .options('/sse') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'POST'); + + expect(response.status).toBe(204); + }); + }); + + describe('Connection Lifecycle', () => { + it('should set up close event handler', async () => { + let closeHandlerRegistered = false; + + const customApp = express(); + customApp.use(cors()); + customApp.post('/sse', async (req, res) => { + const server = new McpServer({ + name: 'ynab-mcp-server', + version: VERSION, + }); + + const transport = new SSEServerTransport('/message', res); + await server.connect(transport); + + req.on('close', () => { + closeHandlerRegistered = true; + }); + }); + + await request(customApp) + .post('/sse') + .timeout(500) + .catch(() => {}); + + // Close handler is registered (we can't easily verify it was called) + expect(McpServer).toHaveBeenCalled(); + }); + }); +});