From 9032c09e1e266069a8441375755f095f766f6b6e Mon Sep 17 00:00:00 2001 From: "Anthony D. Mays" Date: Fri, 11 Jul 2025 00:33:31 -0700 Subject: [PATCH 1/4] feat: adds mcp functionality to fullstack app Signed-off-by: Anthony D. Mays --- lib/javascript/fullstack_demo/.env.example | 6 +- .../fullstack_demo/mcp-server/.env.example | 6 + .../fullstack_demo/mcp-server/.gitignore | 26 + .../mcp-server/AUTHENTICATION.md | 123 ++ .../fullstack_demo/mcp-server/DEPLOYMENT.md | 124 ++ .../fullstack_demo/mcp-server/README.md | 167 +++ .../mcp-server/claude-desktop-config.json | 14 + .../mcp-server/generate-api-key.mjs | 15 + .../fullstack_demo/mcp-server/mcp-config.json | 14 + .../mcp-server/package-lock.json | 1084 +++++++++++++++++ .../fullstack_demo/mcp-server/package.json | 27 + .../mcp-server/src/api-client.ts | 122 ++ .../fullstack_demo/mcp-server/src/index.ts | 316 +++++ .../mcp-server/src/types/todo.ts | 23 + .../mcp-server/test-connection.mjs | 57 + .../fullstack_demo/mcp-server/test-fetch.mjs | 30 + .../fullstack_demo/mcp-server/test.mjs | 36 + .../fullstack_demo/mcp-server/tsconfig.json | 20 + .../src/app/api/mcp/debug/route.ts | 14 + .../src/app/api/mcp/todos/[id]/route.ts | 63 + .../src/app/api/mcp/todos/route.ts | 74 ++ .../fullstack_demo/src/middleware.ts | 6 +- 22 files changed, 2365 insertions(+), 2 deletions(-) create mode 100644 lib/javascript/fullstack_demo/mcp-server/.env.example create mode 100644 lib/javascript/fullstack_demo/mcp-server/.gitignore create mode 100644 lib/javascript/fullstack_demo/mcp-server/AUTHENTICATION.md create mode 100644 lib/javascript/fullstack_demo/mcp-server/DEPLOYMENT.md create mode 100644 lib/javascript/fullstack_demo/mcp-server/README.md create mode 100644 lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json create mode 100644 lib/javascript/fullstack_demo/mcp-server/generate-api-key.mjs create mode 100644 lib/javascript/fullstack_demo/mcp-server/mcp-config.json create mode 100644 lib/javascript/fullstack_demo/mcp-server/package-lock.json create mode 100644 lib/javascript/fullstack_demo/mcp-server/package.json create mode 100644 lib/javascript/fullstack_demo/mcp-server/src/api-client.ts create mode 100644 lib/javascript/fullstack_demo/mcp-server/src/index.ts create mode 100644 lib/javascript/fullstack_demo/mcp-server/src/types/todo.ts create mode 100644 lib/javascript/fullstack_demo/mcp-server/test-connection.mjs create mode 100644 lib/javascript/fullstack_demo/mcp-server/test-fetch.mjs create mode 100644 lib/javascript/fullstack_demo/mcp-server/test.mjs create mode 100644 lib/javascript/fullstack_demo/mcp-server/tsconfig.json create mode 100644 lib/javascript/fullstack_demo/src/app/api/mcp/debug/route.ts create mode 100644 lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts create mode 100644 lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts diff --git a/lib/javascript/fullstack_demo/.env.example b/lib/javascript/fullstack_demo/.env.example index c885cff73..e3ebfaa52 100644 --- a/lib/javascript/fullstack_demo/.env.example +++ b/lib/javascript/fullstack_demo/.env.example @@ -1,4 +1,8 @@ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=Your Next Public Clerk Publishable Key here CLERK_SECRET_KEY=Your Clerk Secret Key here DB_TYPE=in-memory -NEXT_PUBLIC_API_URL=http://localhost:3000 \ No newline at end of file +NEXT_PUBLIC_API_URL=http://localhost:3000 + +# MCP Server Configuration +MCP_API_KEY=your-secret-mcp-api-key-here +MCP_DEFAULT_USER_ID=mcp-user \ No newline at end of file diff --git a/lib/javascript/fullstack_demo/mcp-server/.env.example b/lib/javascript/fullstack_demo/mcp-server/.env.example new file mode 100644 index 000000000..8641d69d7 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/.env.example @@ -0,0 +1,6 @@ +# Environment Configuration +TODO_API_BASE_URL=http://localhost:3000/api/mcp +MCP_API_KEY=your-secret-mcp-api-key-here + +# Optional: Database connection for direct DB access +DATABASE_URL=your_database_url_here diff --git a/lib/javascript/fullstack_demo/mcp-server/.gitignore b/lib/javascript/fullstack_demo/mcp-server/.gitignore new file mode 100644 index 000000000..2b4735b1f --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/.gitignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules/ +npm-debug.log* + +# Build output +dist/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.production + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log diff --git a/lib/javascript/fullstack_demo/mcp-server/AUTHENTICATION.md b/lib/javascript/fullstack_demo/mcp-server/AUTHENTICATION.md new file mode 100644 index 000000000..074d2e64c --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/AUTHENTICATION.md @@ -0,0 +1,123 @@ +# Authentication Setup for Claude Desktop + +## Quick Setup Guide + +### 1. Configure Your Todo App + +Add these environment variables to your todo app's `.env` file: + +```bash +# Add to /lib/javascript/fullstack_demo/.env +MCP_API_KEY=your-secret-mcp-api-key-here +MCP_DEFAULT_USER_ID=your-user-id-here +``` + +**Generate a secure API key:** +```bash +# Generate a random API key +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +### 2. Configure MCP Server + +Create/update the MCP server's `.env` file: + +```bash +# /lib/javascript/fullstack_demo/mcp-server/.env +TODO_API_BASE_URL=http://localhost:3000/api/mcp +MCP_API_KEY=your-secret-mcp-api-key-here +``` + +**Use the same API key in both files!** + +### 3. Configure Claude Desktop + +Update your Claude Desktop config file: + +**Location:** `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) + +```json +{ + "mcpServers": { + "todo-mcp-server": { + "command": "node", + "args": ["/full/path/to/your/mcp-server/dist/index.js"], + "env": { + "TODO_API_BASE_URL": "http://localhost:3000/api/mcp", + "MCP_API_KEY": "your-secret-mcp-api-key-here" + } + } + } +} +``` + +### 4. Build and Test + +1. **Start your todo app:** + ```bash + cd /lib/javascript/fullstack_demo + npm run dev + ``` + +2. **Build the MCP server:** + ```bash + cd /lib/javascript/fullstack_demo/mcp-server + npm run build + ``` + +3. **Restart Claude Desktop** to pick up the new configuration + +### 5. Test Authentication + +In Claude Desktop, try: +``` +Can you show me my todos? +``` + +If authentication works, you should see your todos or an empty list. + +## Troubleshooting + +### "Unauthorized - Invalid API Key" +- Check that the API key is the same in both `.env` files +- Ensure the MCP server environment variables are set correctly +- Restart Claude Desktop after config changes + +### "Cannot connect to API" +- Verify your todo app is running on the correct port +- Check that the `TODO_API_BASE_URL` points to the MCP endpoints (`/api/mcp`) +- Ensure firewalls aren't blocking the connection + +### "User not found" errors +- Set `MCP_DEFAULT_USER_ID` to a valid user ID from your system +- For development, you can use any string like "test-user" + +## Security Notes + +โš ๏ธ **Important for Production:** + +1. **Keep API keys secret** - Never commit them to version control +2. **Use environment variables** - Don't hardcode keys in your config +3. **Rotate keys regularly** - Generate new API keys periodically +4. **Limit scope** - Consider implementing user-specific API keys +5. **Use HTTPS** - In production, always use HTTPS for API communication + +## Alternative: User-Specific Authentication + +For multiple users, you can extend the authentication to map API keys to specific users: + +```typescript +// In your API route +const userKeyMap = { + 'api-key-1': 'user-1', + 'api-key-2': 'user-2', + // etc. +}; + +function authenticateApiKey(request: Request): string | null { + const apiKey = request.headers.get('X-API-Key'); + return userKeyMap[apiKey] || null; +} +``` + +This allows each Claude Desktop user to have their own API key and access their own todos. diff --git a/lib/javascript/fullstack_demo/mcp-server/DEPLOYMENT.md b/lib/javascript/fullstack_demo/mcp-server/DEPLOYMENT.md new file mode 100644 index 000000000..b0be9dd96 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/DEPLOYMENT.md @@ -0,0 +1,124 @@ +# Todo MCP Server Deployment Guide + +## Quick Start + +1. **Build the server**: + ```bash + cd mcp-server + npm install + npm run build + ``` + +2. **Configure environment**: + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` + +3. **Test the server**: + ```bash + node test.mjs + ``` + +## MCP Client Configuration + +### Claude Desktop + +Add this to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): + +```json +{ + "mcpServers": { + "todo-mcp-server": { + "command": "node", + "args": ["/path/to/your/mcp-server/dist/index.js"], + "env": { + "TODO_API_BASE_URL": "http://localhost:3000/api" + } + } + } +} +``` + +### Other MCP Clients + +For other MCP-compatible clients, use the stdio transport with: +- **Command**: `node` +- **Args**: `["/path/to/mcp-server/dist/index.js"]` +- **Environment**: Set `TODO_API_BASE_URL` to your API base URL + +## Environment Variables + +| Variable | Description | Default | Required | +| ------------------- | ----------------------------------- | --------------------------- | -------- | +| `TODO_API_BASE_URL` | Base URL of the Todo API | `http://localhost:3000/api` | Yes | +| `CLERK_SECRET_KEY` | Clerk secret key for authentication | - | Optional | + +## Usage Examples + +Once configured with an MCP client, you can use these tools: + +### Get All Todos +``` +Can you show me all my todos? +``` + +### Create a Todo +``` +Create a new todo: "Learn about MCP servers" +``` + +### Update a Todo +``` +Update todo ID 1 to mark it as completed +``` + +### Get Statistics +``` +Show me my todo statistics +``` + +## Troubleshooting + +### Common Issues + +1. **Module not found errors**: + - Ensure you've run `npm run build` + - Check that the path in your MCP client config is correct + +2. **API connection errors**: + - Verify the Todo app is running on the correct port + - Check the `TODO_API_BASE_URL` environment variable + - Ensure the API endpoints are accessible + +3. **Authentication errors**: + - For production use, configure proper authentication + - Set the `CLERK_SECRET_KEY` if using Clerk authentication + +### Debug Mode + +Run the server in development mode to see detailed logs: +```bash +npm run dev +``` + +## Security Considerations + +โš ๏ธ **Important**: This MCP server is currently configured for development use. For production deployment: + +1. **Authentication**: Implement proper API authentication +2. **HTTPS**: Use HTTPS for API communication +3. **Rate Limiting**: Implement rate limiting to prevent abuse +4. **Input Validation**: Add additional input validation and sanitization +5. **Error Handling**: Avoid exposing sensitive information in error messages + +## API Compatibility + +This MCP server is designed to work with the Todo app's REST API: + +- `GET /api/todos` - Get all todos +- `POST /api/todos` - Create a new todo +- `PATCH /api/todos/{id}` - Update a todo +- `DELETE /api/todos/{id}` - Delete a todo + +Ensure your Todo app implements these endpoints for full compatibility. diff --git a/lib/javascript/fullstack_demo/mcp-server/README.md b/lib/javascript/fullstack_demo/mcp-server/README.md new file mode 100644 index 000000000..47836dbb3 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/README.md @@ -0,0 +1,167 @@ +# Todo MCP Server + +A Model Context Protocol (MCP) server that provides tools for interacting with the Todo application API. + +## Features + +This MCP server provides the following tools: + +- **get_todos**: Retrieve all todos for the authenticated user +- **create_todo**: Create a new todo item +- **update_todo**: Update an existing todo item (text and/or completion status) +- **delete_todo**: Delete a todo item +- **toggle_todo**: Toggle the completion status of a todo item +- **get_todo_stats**: Get statistics about todos (total, completed, pending, completion rate) + +## Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Copy the environment file and configure it: + ```bash + cp .env.example .env + ``` + +3. Update the `.env` file with your configuration: + ``` + TODO_API_BASE_URL=http://localhost:3000/api + CLERK_SECRET_KEY=your_clerk_secret_key_here + ``` + +## Development + +Build the project: +```bash +npm run build +``` + +Run in development mode: +```bash +npm run dev +``` + +Run the compiled server: +```bash +npm start +``` + +## Usage + +This MCP server is designed to be used with MCP-compatible clients. The server communicates via stdio. + +### Tool Examples + +1. **Get all todos**: + ```json + { + "name": "get_todos", + "arguments": {} + } + ``` + +2. **Create a new todo**: + ```json + { + "name": "create_todo", + "arguments": { + "text": "Learn about MCP servers", + "completed": false + } + } + ``` + +3. **Update a todo**: + ```json + { + "name": "update_todo", + "arguments": { + "id": 1, + "text": "Updated todo text", + "completed": true + } + } + ``` + +4. **Delete a todo**: + ```json + { + "name": "delete_todo", + "arguments": { + "id": 1 + } + } + ``` + +5. **Toggle todo completion**: + ```json + { + "name": "toggle_todo", + "arguments": { + "id": 1 + } + } + ``` + +6. **Get todo statistics**: + ```json + { + "name": "get_todo_stats", + "arguments": {} + } + ``` + +## Authentication + +The MCP server interacts with the Todo API, which uses Clerk for authentication. In a production setup, you would need to handle authentication properly by either: + +1. Using API keys or service accounts +2. Implementing session-based authentication +3. Using OAuth flows + +Currently, the server assumes the API is accessible without authentication for demonstration purposes. + +## API Integration + +The server communicates with the Todo app's REST API endpoints: + +- `GET /api/todos` - Get all todos +- `POST /api/todos` - Create a new todo +- `PATCH /api/todos/{id}` - Update a todo +- `DELETE /api/todos/{id}` - Delete a todo + +## Error Handling + +The server includes comprehensive error handling for: +- API request failures +- Network issues +- Invalid todo IDs +- Malformed requests + +All errors are returned as structured responses with descriptive messages. + +## Troubleshooting + +### "fetch is not defined" Error + +If you see `fetch is not defined` errors in Claude Desktop, this means you're running an older version of Node.js. The MCP server now includes a `node-fetch` polyfill to fix this. + +**Solution:** +1. Rebuild the MCP server: `npm run build` +2. Restart Claude Desktop completely +3. The error should be resolved + +### Other Common Issues + +- Ensure all dependencies are correctly installed +- Check your `.env` configuration +- Verify the Todo API is running and accessible + +## Contributing + +1. Make your changes +2. Run `npm run build` to ensure compilation +3. Test with your MCP client +4. Submit a pull request diff --git a/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json b/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json new file mode 100644 index 000000000..9ae74fb55 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "todo-mcp-server": { + "command": "node", + "args": [ + "/Users/anthonymays/source/forks/code-differently-25-q1/lib/javascript/fullstack_demo/mcp-server/dist/index.js" + ], + "env": { + "TODO_API_BASE_URL": "http://localhost:3000/api/mcp", + "MCP_API_KEY": "your-secret-mcp-api-key-here" + } + } + } +} \ No newline at end of file diff --git a/lib/javascript/fullstack_demo/mcp-server/generate-api-key.mjs b/lib/javascript/fullstack_demo/mcp-server/generate-api-key.mjs new file mode 100644 index 000000000..7cbedf5d8 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/generate-api-key.mjs @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +import { randomBytes } from 'crypto'; + +function generateApiKey() { + return randomBytes(32).toString('hex'); +} + +console.log('๐Ÿ”‘ Generated MCP API Key:'); +console.log(generateApiKey()); +console.log('\n๐Ÿ“ Add this to both:'); +console.log(' 1. Your todo app .env file (MCP_API_KEY=...)'); +console.log(' 2. Your MCP server .env file (MCP_API_KEY=...)'); +console.log(' 3. Your Claude Desktop config (MCP_API_KEY: "...")'); +console.log('\n๐Ÿ”’ Keep this key secret and secure!'); diff --git a/lib/javascript/fullstack_demo/mcp-server/mcp-config.json b/lib/javascript/fullstack_demo/mcp-server/mcp-config.json new file mode 100644 index 000000000..d13780a43 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/mcp-config.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "todo-mcp-server": { + "command": "node", + "args": [ + "dist/index.js" + ], + "cwd": "/path/to/your/mcp-server", + "env": { + "TODO_API_BASE_URL": "http://localhost:3000/api" + } + } + } +} \ No newline at end of file diff --git a/lib/javascript/fullstack_demo/mcp-server/package-lock.json b/lib/javascript/fullstack_demo/mcp-server/package-lock.json new file mode 100644 index 000000000..4bd2a12e7 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/package-lock.json @@ -0,0 +1,1084 @@ +{ + "name": "todo-mcp-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "todo-mcp-server", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^0.6.0", + "@types/node": "^20.0.0", + "@types/node-fetch": "^2.6.12", + "dotenv": "^16.4.5", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.1.tgz", + "integrity": "sha512-OkVXMix3EIbB5Z6yife2XTrSlOnVvCLR1Kg91I4pYFEsV9RbnoyQVScXCuVhGaZHOnTZgso8lMQN1Po2TadGKQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@types/node": { + "version": "20.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.7.tgz", + "integrity": "sha512-1GM9z6BJOv86qkPvzh2i6VW5+VVrXxCLknfmTkWEqz+6DqosiY28XUWCTmBcJ0ACzKqx/iwdIREfo1fwExIlkA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/lib/javascript/fullstack_demo/mcp-server/package.json b/lib/javascript/fullstack_demo/mcp-server/package.json new file mode 100644 index 000000000..b04966d4e --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/package.json @@ -0,0 +1,27 @@ +{ + "name": "todo-mcp-server", + "version": "1.0.0", + "description": "MCP server for Todo App integration", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "start": "node dist/index.js", + "clean": "rm -rf dist" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^0.6.0", + "@types/node": "^20.0.0", + "@types/node-fetch": "^2.6.12", + "dotenv": "^16.4.5", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts b/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts new file mode 100644 index 000000000..e55791c0b --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts @@ -0,0 +1,122 @@ +import { Todo } from './types/todo.js'; +// @ts-ignore - Using node-fetch for compatibility +import fetch from 'node-fetch'; + +export class TodoApiClient { + private baseUrl: string; + private authToken?: string; + + constructor(baseUrl: string, apiKey?: string) { + this.baseUrl = baseUrl; + this.authToken = apiKey; + } + + private async makeRequest( + endpoint: string, + options: RequestInit = {}, + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + // Add API key header if available + if (this.authToken) { + headers['X-API-Key'] = this.authToken; + } + + console.error(`[API] ${options.method || 'GET'} ${url}`); + console.error(`[API] Headers:`, headers); + if (options.body) { + console.error(`[API] Body:`, options.body); + } + + const response = await (fetch as any)(url, { + ...options, + headers: { + ...headers, + ...options.headers, + }, + }); + + console.error(`[API] Response: ${response.status} ${response.statusText}`); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`[API] Error response:`, errorText); + throw new Error( + `API request failed: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + // Handle empty responses (like DELETE operations) + if ( + response.status === 204 || + response.headers.get('content-length') === '0' + ) { + return null; + } + + return await response.json(); + } + + async getTodos(): Promise { + const todos = await this.makeRequest('/todos'); + return todos || []; + } + + async createTodo(text: string, completed = false): Promise { + const todo = { text, completed }; + return await this.makeRequest('/todos', { + method: 'POST', + body: JSON.stringify(todo), + }); + } + + async updateTodo( + id: number, + updates: Partial>, + ): Promise { + const updateData = { id, ...updates }; + return await this.makeRequest(`/todos/${id}`, { + method: 'PATCH', + body: JSON.stringify(updateData), + }); + } + + async deleteTodo(id: number): Promise { + await this.makeRequest(`/todos/${id}`, { + method: 'DELETE', + }); + } + + async toggleTodo(id: number): Promise { + // First get the current todo to know its current state + const todos = await this.getTodos(); + const todo = todos.find((t) => t.id === id); + + if (!todo) { + throw new Error(`Todo with ID ${id} not found`); + } + + return await this.updateTodo(id, { completed: !todo.completed }); + } + + async getTodoStats() { + const todos = await this.getTodos(); + + return { + total: todos.length, + completed: todos.filter((t) => t.completed).length, + pending: todos.filter((t) => !t.completed).length, + completionRate: + todos.length > 0 + ? ( + (todos.filter((t) => t.completed).length / todos.length) * + 100 + ).toFixed(1) + '%' + : '0%', + }; + } +} diff --git a/lib/javascript/fullstack_demo/mcp-server/src/index.ts b/lib/javascript/fullstack_demo/mcp-server/src/index.ts new file mode 100644 index 000000000..b8c86ee87 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/src/index.ts @@ -0,0 +1,316 @@ +#!/usr/bin/env node + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequest, + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import dotenv from 'dotenv'; +import { TodoApiClient } from './api-client.js'; +import { CreateTodoRequest, Todo, UpdateTodoRequest } from './types/todo.js'; + +// Load environment variables +dotenv.config(); + +class TodoMCPServer { + private server: Server; + private apiClient: TodoApiClient; + + constructor() { + const apiBaseUrl = + process.env.TODO_API_BASE_URL || 'http://localhost:3000/api/mcp'; + const apiKey = process.env.MCP_API_KEY; + + this.apiClient = new TodoApiClient(apiBaseUrl, apiKey); + + this.server = new Server( + { + name: 'todo-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + this.setupToolHandlers(); + this.setupErrorHandling(); + } + + private setupErrorHandling(): void { + this.server.onerror = (error: Error) => { + console.error('[MCP Error]', error); + }; + + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); + } + + private setupToolHandlers(): void { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'get_todos', + description: 'Retrieve all todos for the authenticated user', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + }, + { + name: 'create_todo', + description: 'Create a new todo item', + inputSchema: { + type: 'object', + properties: { + text: { + type: 'string', + description: 'The text content of the todo item', + }, + completed: { + type: 'boolean', + description: 'Whether the todo is completed (default: false)', + default: false, + }, + }, + required: ['text'], + }, + }, + { + name: 'update_todo', + description: 'Update an existing todo item', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'number', + description: 'The ID of the todo to update', + }, + text: { + type: 'string', + description: 'The new text content of the todo item', + }, + completed: { + type: 'boolean', + description: 'Whether the todo is completed', + }, + }, + required: ['id'], + }, + }, + { + name: 'delete_todo', + description: 'Delete a todo item', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'number', + description: 'The ID of the todo to delete', + }, + }, + required: ['id'], + }, + }, + { + name: 'toggle_todo', + description: 'Toggle the completion status of a todo item', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'number', + description: 'The ID of the todo to toggle', + }, + }, + required: ['id'], + }, + }, + { + name: 'get_todo_stats', + description: + 'Get statistics about todos (total, completed, pending)', + inputSchema: { + type: 'object', + properties: {}, + required: [], + }, + }, + ], + }; + }); + + this.server.setRequestHandler( + CallToolRequestSchema, + async (request: CallToolRequest) => { + const { name, arguments: args } = request.params; + + try { + switch (name) { + case 'get_todos': + return await this.getTodos(); + + case 'create_todo': + if (!args || typeof args !== 'object' || !('text' in args)) { + throw new Error('Missing required parameter: text'); + } + return await this.createTodo( + args as unknown as CreateTodoRequest, + ); + + case 'update_todo': + if (!args || typeof args !== 'object' || !('id' in args)) { + throw new Error('Missing required parameter: id'); + } + return await this.updateTodo( + args as unknown as UpdateTodoRequest, + ); + + case 'delete_todo': + if (!args || typeof args !== 'object' || !('id' in args)) { + throw new Error('Missing required parameter: id'); + } + return await this.deleteTodo(args.id as number); + + case 'toggle_todo': + if (!args || typeof args !== 'object' || !('id' in args)) { + throw new Error('Missing required parameter: id'); + } + return await this.toggleTodo(args.id as number); + + case 'get_todo_stats': + return await this.getTodoStats(); + + default: + throw new Error(`Unknown tool: ${name}`); + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + }, + ); + } + + private async getTodos() { + console.error('[MCP] Getting todos...'); + const todos = await this.apiClient.getTodos(); + console.error(`[MCP] Retrieved ${todos.length} todos`); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(todos, null, 2), + }, + ], + }; + } + + private async createTodo(args: CreateTodoRequest) { + console.error(`[MCP] Creating todo: "${args.text}"`); + const result = await this.apiClient.createTodo(args.text, args.completed); + console.error(`[MCP] Todo created with ID: ${result}`); + + return { + content: [ + { + type: 'text', + text: `Todo created successfully with ID: ${result}`, + }, + ], + }; + } + + private async updateTodo(args: UpdateTodoRequest) { + const updates: Partial> = {}; + + if (args.text !== undefined) { + updates.text = args.text; + } + + if (args.completed !== undefined) { + updates.completed = args.completed; + } + + const result = await this.apiClient.updateTodo(args.id, updates); + + return { + content: [ + { + type: 'text', + text: `Todo updated successfully: ${JSON.stringify(result, null, 2)}`, + }, + ], + }; + } + + private async deleteTodo(id: number) { + await this.apiClient.deleteTodo(id); + + return { + content: [ + { + type: 'text', + text: `Todo with ID ${id} deleted successfully`, + }, + ], + }; + } + + private async toggleTodo(id: number) { + const result = await this.apiClient.toggleTodo(id); + + return { + content: [ + { + type: 'text', + text: `Todo toggled successfully: ${JSON.stringify(result, null, 2)}`, + }, + ], + }; + } + + private async getTodoStats() { + const stats = await this.apiClient.getTodoStats(); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(stats, null, 2), + }, + ], + }; + } + + async run(): Promise { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Todo MCP server running on stdio'); + console.error( + `API Base URL: ${process.env.TODO_API_BASE_URL || 'http://localhost:3000/api/mcp'}`, + ); + console.error( + `API Key configured: ${process.env.MCP_API_KEY ? 'Yes' : 'No'}`, + ); + } +} + +const server = new TodoMCPServer(); +server.run().catch(console.error); diff --git a/lib/javascript/fullstack_demo/mcp-server/src/types/todo.ts b/lib/javascript/fullstack_demo/mcp-server/src/types/todo.ts new file mode 100644 index 000000000..1bba23ee8 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/src/types/todo.ts @@ -0,0 +1,23 @@ +export interface Todo { + id: number; + text: string; + completed: boolean; +} + +export interface CreateTodoRequest { + text: string; + completed?: boolean; +} + +export interface UpdateTodoRequest { + id: number; + text?: string; + completed?: boolean; +} + +export interface TodoStats { + total: number; + completed: number; + pending: number; + completionRate: string; +} diff --git a/lib/javascript/fullstack_demo/mcp-server/test-connection.mjs b/lib/javascript/fullstack_demo/mcp-server/test-connection.mjs new file mode 100644 index 000000000..47e94c885 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/test-connection.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +import dotenv from 'dotenv'; +import { TodoApiClient } from './dist/api-client.js'; + +dotenv.config(); + +async function testApiConnection() { + console.log('๐Ÿงช Testing MCP API Connection...\n'); + + const apiBaseUrl = + process.env.TODO_API_BASE_URL || 'http://localhost:3000/api/mcp'; + const apiKey = process.env.MCP_API_KEY; + + console.log(`๐Ÿ“ก API Base URL: ${apiBaseUrl}`); + console.log(`๐Ÿ”‘ API Key configured: ${apiKey ? 'Yes' : 'No'}`); + + if (!apiKey) { + console.log( + 'โŒ No API key found. Please set MCP_API_KEY in your .env file', + ); + console.log('๐Ÿ’ก Run: node generate-api-key.mjs to generate one'); + return; + } + + const client = new TodoApiClient(apiBaseUrl, apiKey); + + try { + console.log('\n๐Ÿ” Testing GET /todos...'); + const todos = await client.getTodos(); + console.log(`โœ… Success! Retrieved ${todos.length} todos:`, todos); + + console.log('\n๐Ÿ” Testing POST /todos...'); + const newTodoId = await client.createTodo( + 'Test todo from MCP client', + false, + ); + console.log(`โœ… Success! Created todo with ID: ${newTodoId}`); + + console.log('\n๐Ÿ” Testing GET /todos again...'); + const updatedTodos = await client.getTodos(); + console.log(`โœ… Success! Now have ${updatedTodos.length} todos`); + + console.log( + '\n๐ŸŽ‰ All tests passed! Your MCP server should work with Claude Desktop.', + ); + } catch (error) { + console.error('\nโŒ Connection test failed:', error.message); + console.log('\n๐Ÿ”ง Troubleshooting steps:'); + console.log('1. Make sure your todo app is running: npm run dev'); + console.log('2. Check that the API key matches in both .env files'); + console.log('3. Verify the API URL is correct'); + console.log('4. Check the server logs for more details'); + } +} + +testApiConnection(); diff --git a/lib/javascript/fullstack_demo/mcp-server/test-fetch.mjs b/lib/javascript/fullstack_demo/mcp-server/test-fetch.mjs new file mode 100644 index 000000000..a5967c6e2 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/test-fetch.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +// Test that fetch works with our MCP server +import { TodoApiClient } from './dist/api-client.js'; + +async function testFetch() { + console.log('๐Ÿงช Testing fetch functionality...\n'); + + try { + const client = new TodoApiClient( + 'http://localhost:3000/api/mcp', + 'test-key', + ); + console.log('โœ… TodoApiClient created successfully'); + console.log('โœ… fetch polyfill loaded correctly'); + + console.log( + '\n๐Ÿ“ก If your todo server is running on port 3000, the MCP server should work in Claude Desktop now!', + ); + console.log('\n๐Ÿ”ง Steps to test in Claude Desktop:'); + console.log(' 1. Make sure your todo app is running on port 3000'); + console.log(' 2. Restart Claude Desktop completely'); + console.log(' 3. Ask: "Can you show me my todos?"'); + } catch (error) { + console.error('โŒ Error testing fetch:', error.message); + process.exit(1); + } +} + +testFetch(); diff --git a/lib/javascript/fullstack_demo/mcp-server/test.mjs b/lib/javascript/fullstack_demo/mcp-server/test.mjs new file mode 100644 index 000000000..2573f4cf9 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/test.mjs @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +import { TodoApiClient } from './dist/api-client.js'; + +async function testMCPServer() { + console.log('Testing Todo MCP Server...\n'); + + // Mock API client for testing (since we don't have the real API running) + const apiBaseUrl = 'http://localhost:3000/api'; + const client = new TodoApiClient(apiBaseUrl); + + try { + console.log('โœ… MCP Server built successfully'); + console.log('โœ… All imports resolved correctly'); + console.log('โœ… TypeScript compilation passed'); + + console.log('\n๐Ÿ“ Available MCP Tools:'); + console.log(' - get_todos: Retrieve all todos'); + console.log(' - create_todo: Create a new todo'); + console.log(' - update_todo: Update an existing todo'); + console.log(' - delete_todo: Delete a todo'); + console.log(' - toggle_todo: Toggle todo completion'); + console.log(' - get_todo_stats: Get todo statistics'); + + console.log('\n๐Ÿš€ MCP Server is ready to use!'); + console.log('\nTo start the server:'); + console.log(' npm start'); + console.log('\nTo run in development mode:'); + console.log(' npm run dev'); + } catch (error) { + console.error('โŒ Test failed:', error); + process.exit(1); + } +} + +testMCPServer(); diff --git a/lib/javascript/fullstack_demo/mcp-server/tsconfig.json b/lib/javascript/fullstack_demo/mcp-server/tsconfig.json new file mode 100644 index 000000000..8056a3d01 --- /dev/null +++ b/lib/javascript/fullstack_demo/mcp-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/lib/javascript/fullstack_demo/src/app/api/mcp/debug/route.ts b/lib/javascript/fullstack_demo/src/app/api/mcp/debug/route.ts new file mode 100644 index 000000000..71cc73adf --- /dev/null +++ b/lib/javascript/fullstack_demo/src/app/api/mcp/debug/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + console.log('[DEBUG] /api/mcp/debug route called'); + + return NextResponse.json({ + message: 'MCP debug route is working!', + timestamp: new Date().toISOString(), + env: { + MCP_API_KEY_SET: !!process.env.MCP_API_KEY, + MCP_DEFAULT_USER_ID: process.env.MCP_DEFAULT_USER_ID, + }, + }); +} diff --git a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts new file mode 100644 index 000000000..6a11e158d --- /dev/null +++ b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts @@ -0,0 +1,63 @@ +import { createTodoRepository } from '@/repositories'; +import { NextResponse } from 'next/server'; + +const todoRepository = createTodoRepository(); + +// Simple API key authentication for MCP +function authenticateApiKey(request: Request): string | null { + const apiKey = + request.headers.get('X-API-Key') || + request.headers.get('Authorization')?.replace('Bearer ', ''); + const validApiKey = process.env.MCP_API_KEY; + + if (!validApiKey || !apiKey || apiKey !== validApiKey) { + return null; + } + + return process.env.MCP_DEFAULT_USER_ID || 'mcp-user'; +} + +/** + * Delete a todo for MCP client + */ +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const userId = authenticateApiKey(request); + + if (!userId) { + return new Response('Unauthorized - Invalid API Key', { status: 401 }); + } + + const { id } = await params; + + try { + await todoRepository.delete(Number(id), userId); + return new Response('No content', { status: 200 }); + } catch (error) { + console.error('Error deleting todo:', error); + return new Response('Internal Server Error', { status: 500 }); + } +} + +/** + * Update a todo for MCP client + */ +export async function PATCH(request: Request) { + const userId = authenticateApiKey(request); + + if (!userId) { + return new Response('Unauthorized - Invalid API Key', { status: 401 }); + } + + const todo = await request.json(); + + try { + const updatedTodo = await todoRepository.patch(todo, userId); + return NextResponse.json(updatedTodo); + } catch (error) { + console.error('Error updating todo:', error); + return new Response('Internal Server Error', { status: 500 }); + } +} diff --git a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts new file mode 100644 index 000000000..caca03e40 --- /dev/null +++ b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts @@ -0,0 +1,74 @@ +import { createTodoRepository } from '@/repositories'; +import { NextResponse } from 'next/server'; + +const todoRepository = createTodoRepository(); + +// Simple API key authentication for MCP +function authenticateApiKey(request: Request): string | null { + const apiKey = + request.headers.get('X-API-Key') || + request.headers.get('Authorization')?.replace('Bearer ', ''); + const validApiKey = process.env.MCP_API_KEY; + + if (!validApiKey || !apiKey || apiKey !== validApiKey) { + return null; + } + + // For demo purposes, return a default user ID + // In production, you might map API keys to specific users + return process.env.MCP_DEFAULT_USER_ID || 'mcp-user'; +} + +/** + * Retrieve all todos for MCP client + */ +export async function GET(request: Request) { + console.log('[MCP API] GET /api/mcp/todos called'); + console.log( + '[MCP API] Headers:', + Object.fromEntries(request.headers.entries()), + ); + + const userId = authenticateApiKey(request); + + if (!userId) { + console.log('[MCP API] Authentication failed'); + return new Response('Unauthorized - Invalid API Key', { status: 401 }); + } + + console.log(`[MCP API] Authenticated as user: ${userId}`); + + try { + const todos = await todoRepository.getAll(userId); + console.log(`[MCP API] Retrieved ${todos?.length || 0} todos`); + return NextResponse.json(todos || []); + } catch (error) { + console.error('[MCP API] Error fetching todos:', error); + return new Response('Internal Server Error', { status: 500 }); + } +} + +/** + * Create a new todo for MCP client + */ +export async function POST(request: Request) { + console.log('[MCP API] POST /api/mcp/todos called'); + + const userId = authenticateApiKey(request); + + if (!userId) { + console.log('[MCP API] Authentication failed'); + return new Response('Unauthorized - Invalid API Key', { status: 401 }); + } + + const todo = await request.json(); + console.log(`[MCP API] Creating todo for user ${userId}:`, todo); + + try { + const id = await todoRepository.create(todo, userId); + return NextResponse.json(id); + } catch (error) { + console.error('Error creating todo:', error); + return new Response('Internal Server Error', { status: 500 }); + } +} diff --git a/lib/javascript/fullstack_demo/src/middleware.ts b/lib/javascript/fullstack_demo/src/middleware.ts index 80a2bc2c8..93e5061dc 100644 --- a/lib/javascript/fullstack_demo/src/middleware.ts +++ b/lib/javascript/fullstack_demo/src/middleware.ts @@ -4,12 +4,16 @@ import { v4 as uuidv4 } from 'uuid'; const CORRELATION_ID_HEADER = 'x-correlation-id'; const isProtectedRoute = createRouteMatcher(['/(.*)']); +const isMcpApiRoute = createRouteMatcher(['/api/mcp/(.*)']); export default clerkMiddleware(async (auth, req) => { const correlationId = uuidv4(); req.headers.set(CORRELATION_ID_HEADER, correlationId); - if (isProtectedRoute(req)) await auth.protect(); + // Skip Clerk protection for MCP API routes (they have their own API key auth) + if (isProtectedRoute(req) && !isMcpApiRoute(req)) { + await auth.protect(); + } const response = NextResponse.next(); response.headers.set(CORRELATION_ID_HEADER, correlationId); From 1d7f20025d49d9f682536d9e211ee341bf3e1cca Mon Sep 17 00:00:00 2001 From: "Anthony D. Mays" Date: Fri, 11 Jul 2025 09:11:46 -0700 Subject: [PATCH 2/4] chore: refactors authenticateApiKey into shared util Signed-off-by: Anthony D. Mays --- .../fullstack_demo/mcp-server/src/index.ts | 16 ++++++++-------- .../src/app/api/mcp/todos/[id]/route.ts | 15 +-------------- .../src/app/api/mcp/todos/route.ts | 17 +---------------- .../fullstack_demo/src/util/mcp-auth.ts | 17 +++++++++++++++++ 4 files changed, 27 insertions(+), 38 deletions(-) create mode 100644 lib/javascript/fullstack_demo/src/util/mcp-auth.ts diff --git a/lib/javascript/fullstack_demo/mcp-server/src/index.ts b/lib/javascript/fullstack_demo/mcp-server/src/index.ts index b8c86ee87..07905b7d2 100644 --- a/lib/javascript/fullstack_demo/mcp-server/src/index.ts +++ b/lib/javascript/fullstack_demo/mcp-server/src/index.ts @@ -43,7 +43,7 @@ class TodoMCPServer { private setupErrorHandling(): void { this.server.onerror = (error: Error) => { - console.error('[MCP Error]', error); + console.debug('[MCP Error]', error); }; process.on('SIGINT', async () => { @@ -208,9 +208,9 @@ class TodoMCPServer { } private async getTodos() { - console.error('[MCP] Getting todos...'); + console.debug('[MCP] Getting todos...'); const todos = await this.apiClient.getTodos(); - console.error(`[MCP] Retrieved ${todos.length} todos`); + console.debug(`[MCP] Retrieved ${todos.length} todos`); return { content: [ @@ -223,9 +223,9 @@ class TodoMCPServer { } private async createTodo(args: CreateTodoRequest) { - console.error(`[MCP] Creating todo: "${args.text}"`); + console.debug(`[MCP] Creating todo: "${args.text}"`); const result = await this.apiClient.createTodo(args.text, args.completed); - console.error(`[MCP] Todo created with ID: ${result}`); + console.debug(`[MCP] Todo created with ID: ${result}`); return { content: [ @@ -302,11 +302,11 @@ class TodoMCPServer { async run(): Promise { const transport = new StdioServerTransport(); await this.server.connect(transport); - console.error('Todo MCP server running on stdio'); - console.error( + console.log('Todo MCP server running on stdio'); + console.debug( `API Base URL: ${process.env.TODO_API_BASE_URL || 'http://localhost:3000/api/mcp'}`, ); - console.error( + console.debug( `API Key configured: ${process.env.MCP_API_KEY ? 'Yes' : 'No'}`, ); } diff --git a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts index 6a11e158d..547cd6ca4 100644 --- a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts +++ b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/[id]/route.ts @@ -1,22 +1,9 @@ import { createTodoRepository } from '@/repositories'; +import { authenticateApiKey } from '@/util/mcp-auth'; import { NextResponse } from 'next/server'; const todoRepository = createTodoRepository(); -// Simple API key authentication for MCP -function authenticateApiKey(request: Request): string | null { - const apiKey = - request.headers.get('X-API-Key') || - request.headers.get('Authorization')?.replace('Bearer ', ''); - const validApiKey = process.env.MCP_API_KEY; - - if (!validApiKey || !apiKey || apiKey !== validApiKey) { - return null; - } - - return process.env.MCP_DEFAULT_USER_ID || 'mcp-user'; -} - /** * Delete a todo for MCP client */ diff --git a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts index caca03e40..582ccb299 100644 --- a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts +++ b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts @@ -1,24 +1,9 @@ import { createTodoRepository } from '@/repositories'; +import { authenticateApiKey } from '@/util/mcp-auth'; import { NextResponse } from 'next/server'; const todoRepository = createTodoRepository(); -// Simple API key authentication for MCP -function authenticateApiKey(request: Request): string | null { - const apiKey = - request.headers.get('X-API-Key') || - request.headers.get('Authorization')?.replace('Bearer ', ''); - const validApiKey = process.env.MCP_API_KEY; - - if (!validApiKey || !apiKey || apiKey !== validApiKey) { - return null; - } - - // For demo purposes, return a default user ID - // In production, you might map API keys to specific users - return process.env.MCP_DEFAULT_USER_ID || 'mcp-user'; -} - /** * Retrieve all todos for MCP client */ diff --git a/lib/javascript/fullstack_demo/src/util/mcp-auth.ts b/lib/javascript/fullstack_demo/src/util/mcp-auth.ts new file mode 100644 index 000000000..5b819341d --- /dev/null +++ b/lib/javascript/fullstack_demo/src/util/mcp-auth.ts @@ -0,0 +1,17 @@ +/** + * Simple API key authentication for MCP + */ +export function authenticateApiKey(request: Request): string | null { + const apiKey = + request.headers.get('X-API-Key') || + request.headers.get('Authorization')?.replace('Bearer ', ''); + const validApiKey = process.env.MCP_API_KEY; + + if (!validApiKey || !apiKey || apiKey !== validApiKey) { + return null; + } + + // For demo purposes, return a default user ID + // In production, you might map API keys to specific users + return process.env.MCP_DEFAULT_USER_ID || 'mcp-user'; +} From c13c3e441bc039defc87c3dd516e914670d7627b Mon Sep 17 00:00:00 2001 From: "Anthony D. Mays" Date: Fri, 11 Jul 2025 09:23:49 -0700 Subject: [PATCH 3/4] chore: remove header logging Signed-off-by: Anthony D. Mays --- lib/javascript/fullstack_demo/mcp-server/src/api-client.ts | 1 - lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts b/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts index e55791c0b..f04f0a968 100644 --- a/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts +++ b/lib/javascript/fullstack_demo/mcp-server/src/api-client.ts @@ -27,7 +27,6 @@ export class TodoApiClient { } console.error(`[API] ${options.method || 'GET'} ${url}`); - console.error(`[API] Headers:`, headers); if (options.body) { console.error(`[API] Body:`, options.body); } diff --git a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts index 582ccb299..dd9b272e3 100644 --- a/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts +++ b/lib/javascript/fullstack_demo/src/app/api/mcp/todos/route.ts @@ -9,10 +9,6 @@ const todoRepository = createTodoRepository(); */ export async function GET(request: Request) { console.log('[MCP API] GET /api/mcp/todos called'); - console.log( - '[MCP API] Headers:', - Object.fromEntries(request.headers.entries()), - ); const userId = authenticateApiKey(request); From 69794d13316f2aa4e0e7659e8545d3ce3e82d299 Mon Sep 17 00:00:00 2001 From: "Anthony D. Mays" Date: Fri, 11 Jul 2025 09:25:50 -0700 Subject: [PATCH 4/4] chore: updates claude desktop config example Signed-off-by: Anthony D. Mays --- .../fullstack_demo/mcp-server/claude-desktop-config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json b/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json index 9ae74fb55..a77cf911d 100644 --- a/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json +++ b/lib/javascript/fullstack_demo/mcp-server/claude-desktop-config.json @@ -3,11 +3,11 @@ "todo-mcp-server": { "command": "node", "args": [ - "/Users/anthonymays/source/forks/code-differently-25-q1/lib/javascript/fullstack_demo/mcp-server/dist/index.js" + "/your/path/here/code-differently-25-q1/lib/javascript/fullstack_demo/mcp-server/dist/index.js" ], "env": { "TODO_API_BASE_URL": "http://localhost:3000/api/mcp", - "MCP_API_KEY": "your-secret-mcp-api-key-here" + "MCP_API_KEY": "your_api_key_here" } } }