feat: 初次提交
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
237
README.md
Normal file
237
README.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Cloud MCP - Personal MCP Server
|
||||
|
||||
A comprehensive personal MCP (Model Context Protocol) server built with Bun and TypeScript, designed for developers, DevOps engineers, and families.
|
||||
|
||||
## Features
|
||||
|
||||
### 🚀 Programming Tools
|
||||
- **Code Snippet Management**: Save, search, list, and delete code snippets with tags and categories
|
||||
- **Project Templates**: Quickly generate Vite + Vue3 + TypeScript projects (frontend or fullstack)
|
||||
- **Technical Documentation**: Quick access to TypeScript, Vue3, and Bun documentation
|
||||
- **Code Review**: Get code review suggestions and optimization tips
|
||||
|
||||
### 🔧 DevOps Tools
|
||||
- **NAS Management**: List, upload, download, and search files on your NAS
|
||||
- **Server Monitoring**: Check cloud server status (CPU, memory, disk) and view logs
|
||||
- **Router Management**: Monitor soft router status, traffic, and connected devices
|
||||
|
||||
### 👨👩👦 Family Tools
|
||||
- **Math Resources**: Search and save math teaching resources, generate problems by grade
|
||||
- **Baby Milestones**: Record and track baby milestones and set reminders
|
||||
|
||||
### ⚽ Hobby Tools
|
||||
- **Football Information**: Get match schedules, team info, and league standings
|
||||
- **Game Information**: Search game info, find deals, and manage wishlist
|
||||
|
||||
### 📝 Common Tools
|
||||
- **Notes Management**: Create, search, list, and delete personal notes
|
||||
- **Task Management**: Add, list, and complete tasks
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or navigate to the project directory:
|
||||
```bash
|
||||
cd cloud-mcp
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Configure environment variables (optional):
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file in the project root with the following variables (all optional):
|
||||
|
||||
```env
|
||||
# NAS Configuration
|
||||
NAS_HOST=your-nas-ip
|
||||
NAS_USERNAME=your-username
|
||||
NAS_PASSWORD=your-password
|
||||
NAS_PROTOCOL=smb # smb, ftp, or sftp
|
||||
|
||||
# Cloud Server Configuration
|
||||
SERVER_HOST=your-server-ip
|
||||
SERVER_USERNAME=your-username
|
||||
SERVER_PORT=22
|
||||
SERVER_KEY_PATH=/path/to/ssh/key
|
||||
|
||||
# Soft Router Configuration
|
||||
ROUTER_HOST=your-router-ip
|
||||
ROUTER_USERNAME=admin
|
||||
ROUTER_PASSWORD=your-password
|
||||
|
||||
# API Keys (optional)
|
||||
FOOTBALL_API_KEY=your-football-api-key
|
||||
GAME_API_KEY=your-game-api-key
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running the Server
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Or build and run:
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
bun run start
|
||||
```
|
||||
|
||||
### Connecting to MCP Clients
|
||||
|
||||
The server uses stdio transport, so it can be connected to any MCP-compatible client (like Claude Desktop, Cursor, etc.).
|
||||
|
||||
Example configuration for Claude Desktop (`claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"cloud-mcp": {
|
||||
"command": "bun",
|
||||
"args": ["run", "/path/to/cloud-mcp/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Programming
|
||||
- `code_snippet_save` - Save a code snippet
|
||||
- `code_snippet_search` - Search code snippets
|
||||
- `code_snippet_list` - List all code snippets
|
||||
- `code_snippet_delete` - Delete a code snippet
|
||||
- `project_template_create` - Create Vite + Vue3 project
|
||||
- `project_template_create_fullstack` - Create fullstack project
|
||||
- `project_template_list` - List available templates
|
||||
- `docs_typescript` - Get TypeScript documentation
|
||||
- `docs_vue3` - Get Vue3 documentation
|
||||
- `docs_bun` - Get Bun documentation
|
||||
- `code_review` - Review code
|
||||
- `code_optimize` - Get optimization suggestions
|
||||
|
||||
### DevOps
|
||||
- `nas_list_files` - List NAS files
|
||||
- `nas_upload_file` - Upload file to NAS
|
||||
- `nas_download_file` - Download file from NAS
|
||||
- `nas_search_files` - Search files on NAS
|
||||
- `server_status` - Get server status
|
||||
- `server_deploy` - Deploy application
|
||||
- `server_logs` - View server logs
|
||||
- `router_status` - Get router status
|
||||
- `router_traffic` - Get traffic statistics
|
||||
- `router_devices` - List connected devices
|
||||
|
||||
### Family
|
||||
- `math_resource_search` - Search math resources
|
||||
- `math_problem_generate` - Generate math problems
|
||||
- `math_resource_save` - Save math resource
|
||||
- `baby_milestone_add` - Record baby milestone
|
||||
- `baby_milestone_list` - List milestones
|
||||
- `baby_reminder_set` - Set baby reminder
|
||||
|
||||
### Hobbies
|
||||
- `football_matches` - Get football matches
|
||||
- `football_team_info` - Get team information
|
||||
- `football_standings` - Get league standings
|
||||
- `game_info` - Get game information
|
||||
- `game_deals` - Get game deals
|
||||
- `game_wishlist` - Manage game wishlist
|
||||
|
||||
### Common
|
||||
- `note_create` - Create a note
|
||||
- `note_search` - Search notes
|
||||
- `note_list` - List notes
|
||||
- `note_delete` - Delete a note
|
||||
- `task_add` - Add a task
|
||||
- `task_list` - List tasks
|
||||
- `task_complete` - Complete a task
|
||||
|
||||
## Data Storage
|
||||
|
||||
All data is stored locally in JSON files in the `data/` directory:
|
||||
- `codeSnippets.json` - Code snippets
|
||||
- `notes.json` - Personal notes
|
||||
- `tasks.json` - Tasks
|
||||
- `babyMilestones.json` - Baby milestones
|
||||
- `mathResources.json` - Math resources
|
||||
- `gameWishlist.json` - Game wishlist
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
cloud-mcp/
|
||||
├── src/
|
||||
│ ├── index.ts # Entry point
|
||||
│ ├── server.ts # MCP server core
|
||||
│ ├── tools/ # Tool modules
|
||||
│ │ ├── programming/ # Programming tools
|
||||
│ │ ├── devops/ # DevOps tools
|
||||
│ │ ├── family/ # Family tools
|
||||
│ │ ├── hobbies/ # Hobby tools
|
||||
│ │ └── common/ # Common tools
|
||||
│ ├── storage/ # Storage layer
|
||||
│ │ ├── config.ts # Configuration
|
||||
│ │ └── database.ts # Database/storage
|
||||
│ └── utils/ # Utilities
|
||||
│ └── logger.ts # Logging
|
||||
├── data/ # Data files (created at runtime)
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. Create a new tool file in the appropriate directory under `src/tools/`
|
||||
2. Export a registration function (e.g., `registerMyTool`)
|
||||
3. Import and call it in `src/index.ts`
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// src/tools/myfeature/myTool.ts
|
||||
import { mcpServer } from '../../server.js';
|
||||
|
||||
export function registerMyTool(): void {
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'my_tool',
|
||||
description: 'My tool description',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
// Tool implementation
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Result' }],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a personal MCP server, but feel free to fork and customize for your own needs!
|
||||
|
||||
248
bun.lock
Normal file
248
bun.lock
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "cloud-mcp",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.7",
|
||||
"ssh2": "^1.15.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.9",
|
||||
"@types/ssh2": "^1.15.4",
|
||||
"typescript": "^5.6.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@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", "eventsource": "^3.0.2", "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.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
|
||||
|
||||
"@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||
|
||||
"buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.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", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "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" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nan": ["nan@2.24.0", "", {}, "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
}
|
||||
}
|
||||
26
env.template
Normal file
26
env.template
Normal file
@@ -0,0 +1,26 @@
|
||||
# NAS Configuration
|
||||
# 配置你的 NAS 访问信息
|
||||
NAS_HOST=192.168.1.100
|
||||
NAS_USERNAME=admin
|
||||
NAS_PASSWORD=your-nas-password
|
||||
NAS_PROTOCOL=smb
|
||||
|
||||
# Cloud Server Configuration
|
||||
# 配置你的云服务器 SSH 访问信息
|
||||
SERVER_HOST=127.0.01
|
||||
SERVER_USERNAME=root
|
||||
SERVER_PORT=22
|
||||
SERVER_KEY_PATH=/Users/zijianchen/.ssh/id_rsa
|
||||
|
||||
# Soft Router Configuration
|
||||
# 配置你的软路由访问信息
|
||||
ROUTER_HOST=192.168.1.1
|
||||
ROUTER_USERNAME=admin
|
||||
ROUTER_PASSWORD=your-router-password
|
||||
|
||||
# API Keys (optional)
|
||||
# 可选:配置 API 密钥以使用完整功能
|
||||
# 足球信息 API (football-data.org - 免费注册获取)
|
||||
FOOTBALL_API_KEY=
|
||||
# 游戏信息使用免费 API,无需密钥
|
||||
|
||||
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "cloud-mcp",
|
||||
"version": "1.0.0",
|
||||
"description": "Personal MCP server with programming, DevOps, family, and hobby tools",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||
"start": "bun run dist/index.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"personal-assistant"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.7",
|
||||
"ssh2": "^1.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.9",
|
||||
"@types/ssh2": "^1.15.4",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
}
|
||||
}
|
||||
60
src/index.ts
Normal file
60
src/index.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* MCP Server Entry Point
|
||||
* Personal MCP server with programming, DevOps, family, and hobby tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "./server.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
|
||||
// Register all tools
|
||||
import { registerCodeSnippetTools } from "./tools/programming/codeSnippet.js";
|
||||
import { registerProjectTemplateTools } from "./tools/programming/projectTemplate.js";
|
||||
import { registerDocsTools } from "./tools/programming/docs.js";
|
||||
import { registerCodeReviewTools } from "./tools/programming/codeReview.js";
|
||||
|
||||
import { registerNASTools } from "./tools/devops/nas.js";
|
||||
import { registerServerTools } from "./tools/devops/server.js";
|
||||
import { registerRouterTools } from "./tools/devops/router.js";
|
||||
|
||||
import { registerMathTools } from "./tools/family/math.js";
|
||||
import { registerBabyTools } from "./tools/family/baby.js";
|
||||
|
||||
import { registerFootballTools } from "./tools/hobbies/football.js";
|
||||
import { registerGameTools } from "./tools/hobbies/games.js";
|
||||
|
||||
import { registerNoteTools } from "./tools/common/notes.js";
|
||||
import { registerTaskTools } from "./tools/common/tasks.js";
|
||||
|
||||
// Register all tool modules
|
||||
logger.info("Registering tools...");
|
||||
|
||||
// Programming tools
|
||||
registerCodeSnippetTools();
|
||||
registerProjectTemplateTools();
|
||||
registerDocsTools();
|
||||
registerCodeReviewTools();
|
||||
|
||||
// DevOps tools
|
||||
registerNASTools();
|
||||
registerServerTools();
|
||||
registerRouterTools();
|
||||
|
||||
// Family tools
|
||||
registerMathTools();
|
||||
registerBabyTools();
|
||||
|
||||
// Hobby tools
|
||||
registerFootballTools();
|
||||
registerGameTools();
|
||||
|
||||
// Common tools
|
||||
registerNoteTools();
|
||||
registerTaskTools();
|
||||
|
||||
logger.info("All tools registered. Starting MCP server...");
|
||||
|
||||
// Start the server
|
||||
mcpServer.start().catch((error) => {
|
||||
logger.error("Failed to start MCP server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
87
src/server.ts
Normal file
87
src/server.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* MCP Server core implementation
|
||||
*/
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
|
||||
export type ToolHandler = (
|
||||
args: Record<string, unknown>
|
||||
) => Promise<{ content: Array<{ type: string; text: string }> }>;
|
||||
|
||||
class MCPServer {
|
||||
private server: Server;
|
||||
private tools: Map<string, { tool: Tool; handler: ToolHandler }> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: "cloud-mcp",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
const tools: Tool[] = Array.from(this.tools.values()).map(
|
||||
(entry) => entry.tool
|
||||
);
|
||||
return { tools };
|
||||
});
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
const toolEntry = this.tools.get(name);
|
||||
if (!toolEntry) {
|
||||
throw new Error(`Tool ${name} not found`);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Calling tool: ${name}`, args);
|
||||
const result = await toolEntry.handler(args || {});
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`Error executing tool ${name}:`, error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerTool(tool: Tool, handler: ToolHandler): void {
|
||||
this.tools.set(tool.name, { tool, handler });
|
||||
logger.debug(`Registered tool: ${tool.name}`);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
logger.info("MCP Server started");
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpServer = new MCPServer();
|
||||
87
src/storage/config.ts
Normal file
87
src/storage/config.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Configuration management for the MCP server
|
||||
* Handles environment variables and configuration loading
|
||||
*/
|
||||
|
||||
export interface NASConfig {
|
||||
host?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
protocol?: 'smb' | 'ftp' | 'sftp';
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
host?: string;
|
||||
username?: string;
|
||||
port?: number;
|
||||
keyPath?: string;
|
||||
}
|
||||
|
||||
export interface RouterConfig {
|
||||
host?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
nas: NASConfig;
|
||||
server: ServerConfig;
|
||||
router: RouterConfig;
|
||||
footballApiKey?: string;
|
||||
gameApiKey?: string;
|
||||
}
|
||||
|
||||
class ConfigManager {
|
||||
private config: AppConfig;
|
||||
|
||||
constructor() {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
|
||||
private loadConfig(): AppConfig {
|
||||
return {
|
||||
nas: {
|
||||
host: process.env.NAS_HOST,
|
||||
username: process.env.NAS_USERNAME,
|
||||
password: process.env.NAS_PASSWORD,
|
||||
protocol: (process.env.NAS_PROTOCOL as 'smb' | 'ftp' | 'sftp') || 'smb',
|
||||
},
|
||||
server: {
|
||||
host: process.env.SERVER_HOST,
|
||||
username: process.env.SERVER_USERNAME,
|
||||
port: process.env.SERVER_PORT ? parseInt(process.env.SERVER_PORT) : 22,
|
||||
keyPath: process.env.SERVER_KEY_PATH,
|
||||
},
|
||||
router: {
|
||||
host: process.env.ROUTER_HOST,
|
||||
username: process.env.ROUTER_USERNAME,
|
||||
password: process.env.ROUTER_PASSWORD,
|
||||
},
|
||||
footballApiKey: process.env.FOOTBALL_API_KEY,
|
||||
gameApiKey: process.env.GAME_API_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
getConfig(): AppConfig {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
getNASConfig(): NASConfig {
|
||||
return this.config.nas;
|
||||
}
|
||||
|
||||
getServerConfig(): ServerConfig {
|
||||
return this.config.server;
|
||||
}
|
||||
|
||||
getRouterConfig(): RouterConfig {
|
||||
return this.config.router;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager();
|
||||
|
||||
282
src/storage/database.ts
Normal file
282
src/storage/database.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Database/storage layer for the MCP server
|
||||
* Uses JSON file storage for simplicity
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const DATA_DIR = join(process.cwd(), 'data');
|
||||
|
||||
export interface CodeSnippet {
|
||||
id: string;
|
||||
title: string;
|
||||
code: string;
|
||||
language: string;
|
||||
tags: string[];
|
||||
category?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completed: boolean;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export interface BabyMilestone {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface MathResource {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
grade?: string;
|
||||
difficulty?: string;
|
||||
tags: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface GameWishlist {
|
||||
id: string;
|
||||
gameName: string;
|
||||
platform?: string;
|
||||
notes?: string;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
class Database {
|
||||
private ensureDataDir(): void {
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
private getFilePath(collection: string): string {
|
||||
this.ensureDataDir();
|
||||
return join(DATA_DIR, `${collection}.json`);
|
||||
}
|
||||
|
||||
private readCollection<T>(collection: string): T[] {
|
||||
const filePath = this.getFilePath(collection);
|
||||
if (!existsSync(filePath)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error(`Error reading ${collection}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private writeCollection<T>(collection: string, data: T[]): void {
|
||||
const filePath = this.getFilePath(collection);
|
||||
try {
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch (error) {
|
||||
console.error(`Error writing ${collection}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Code Snippets
|
||||
saveCodeSnippet(snippet: CodeSnippet): void {
|
||||
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
|
||||
const index = snippets.findIndex((s) => s.id === snippet.id);
|
||||
if (index >= 0) {
|
||||
snippets[index] = { ...snippet, updatedAt: new Date().toISOString() };
|
||||
} else {
|
||||
snippets.push(snippet);
|
||||
}
|
||||
this.writeCollection('codeSnippets', snippets);
|
||||
}
|
||||
|
||||
getCodeSnippets(): CodeSnippet[] {
|
||||
return this.readCollection<CodeSnippet>('codeSnippets');
|
||||
}
|
||||
|
||||
getCodeSnippet(id: string): CodeSnippet | undefined {
|
||||
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
|
||||
return snippets.find((s) => s.id === id);
|
||||
}
|
||||
|
||||
deleteCodeSnippet(id: string): boolean {
|
||||
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
|
||||
const filtered = snippets.filter((s) => s.id !== id);
|
||||
if (filtered.length < snippets.length) {
|
||||
this.writeCollection('codeSnippets', filtered);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
searchCodeSnippets(query: string, tags?: string[]): CodeSnippet[] {
|
||||
const snippets = this.readCollection<CodeSnippet>('codeSnippets');
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return snippets.filter((s) => {
|
||||
const matchesQuery =
|
||||
s.title.toLowerCase().includes(lowerQuery) ||
|
||||
s.code.toLowerCase().includes(lowerQuery) ||
|
||||
s.language.toLowerCase().includes(lowerQuery);
|
||||
const matchesTags =
|
||||
!tags || tags.length === 0 || tags.some((tag) => s.tags.includes(tag));
|
||||
return matchesQuery && matchesTags;
|
||||
});
|
||||
}
|
||||
|
||||
// Notes
|
||||
saveNote(note: Note): void {
|
||||
const notes = this.readCollection<Note>('notes');
|
||||
const index = notes.findIndex((n) => n.id === note.id);
|
||||
if (index >= 0) {
|
||||
notes[index] = { ...note, updatedAt: new Date().toISOString() };
|
||||
} else {
|
||||
notes.push(note);
|
||||
}
|
||||
this.writeCollection('notes', notes);
|
||||
}
|
||||
|
||||
getNotes(): Note[] {
|
||||
return this.readCollection<Note>('notes');
|
||||
}
|
||||
|
||||
getNote(id: string): Note | undefined {
|
||||
const notes = this.readCollection<Note>('notes');
|
||||
return notes.find((n) => n.id === id);
|
||||
}
|
||||
|
||||
searchNotes(query: string): Note[] {
|
||||
const notes = this.readCollection<Note>('notes');
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return notes.filter(
|
||||
(n) =>
|
||||
n.title.toLowerCase().includes(lowerQuery) ||
|
||||
n.content.toLowerCase().includes(lowerQuery) ||
|
||||
n.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
|
||||
);
|
||||
}
|
||||
|
||||
deleteNote(id: string): boolean {
|
||||
const notes = this.readCollection<Note>('notes');
|
||||
const filtered = notes.filter((n) => n.id !== id);
|
||||
if (filtered.length < notes.length) {
|
||||
this.writeCollection('notes', filtered);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tasks
|
||||
saveTask(task: Task): void {
|
||||
const tasks = this.readCollection<Task>('tasks');
|
||||
const index = tasks.findIndex((t) => t.id === task.id);
|
||||
if (index >= 0) {
|
||||
tasks[index] = task;
|
||||
} else {
|
||||
tasks.push(task);
|
||||
}
|
||||
this.writeCollection('tasks', tasks);
|
||||
}
|
||||
|
||||
getTasks(completed?: boolean): Task[] {
|
||||
const tasks = this.readCollection<Task>('tasks');
|
||||
if (completed === undefined) {
|
||||
return tasks;
|
||||
}
|
||||
return tasks.filter((t) => t.completed === completed);
|
||||
}
|
||||
|
||||
getTask(id: string): Task | undefined {
|
||||
const tasks = this.readCollection<Task>('tasks');
|
||||
return tasks.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
// Baby Milestones
|
||||
saveBabyMilestone(milestone: BabyMilestone): void {
|
||||
const milestones = this.readCollection<BabyMilestone>('babyMilestones');
|
||||
milestones.push(milestone);
|
||||
this.writeCollection('babyMilestones', milestones);
|
||||
}
|
||||
|
||||
getBabyMilestones(): BabyMilestone[] {
|
||||
return this.readCollection<BabyMilestone>('babyMilestones');
|
||||
}
|
||||
|
||||
// Math Resources
|
||||
saveMathResource(resource: MathResource): void {
|
||||
const resources = this.readCollection<MathResource>('mathResources');
|
||||
const index = resources.findIndex((r) => r.id === resource.id);
|
||||
if (index >= 0) {
|
||||
resources[index] = resource;
|
||||
} else {
|
||||
resources.push(resource);
|
||||
}
|
||||
this.writeCollection('mathResources', resources);
|
||||
}
|
||||
|
||||
getMathResources(): MathResource[] {
|
||||
return this.readCollection<MathResource>('mathResources');
|
||||
}
|
||||
|
||||
searchMathResources(query: string, grade?: string): MathResource[] {
|
||||
const resources = this.readCollection<MathResource>('mathResources');
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return resources.filter((r) => {
|
||||
const matchesQuery =
|
||||
r.title.toLowerCase().includes(lowerQuery) ||
|
||||
r.content.toLowerCase().includes(lowerQuery) ||
|
||||
r.tags.some((tag) => tag.toLowerCase().includes(lowerQuery));
|
||||
const matchesGrade = !grade || r.grade === grade;
|
||||
return matchesQuery && matchesGrade;
|
||||
});
|
||||
}
|
||||
|
||||
// Game Wishlist
|
||||
saveGameWishlist(game: GameWishlist): void {
|
||||
const games = this.readCollection<GameWishlist>('gameWishlist');
|
||||
const index = games.findIndex((g) => g.id === game.id);
|
||||
if (index >= 0) {
|
||||
games[index] = game;
|
||||
} else {
|
||||
games.push(game);
|
||||
}
|
||||
this.writeCollection('gameWishlist', games);
|
||||
}
|
||||
|
||||
getGameWishlist(): GameWishlist[] {
|
||||
return this.readCollection<GameWishlist>('gameWishlist');
|
||||
}
|
||||
|
||||
deleteGameWishlist(id: string): boolean {
|
||||
const games = this.readCollection<GameWishlist>('gameWishlist');
|
||||
const filtered = games.filter((g) => g.id !== id);
|
||||
if (filtered.length < games.length) {
|
||||
this.writeCollection('gameWishlist', filtered);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const database = new Database();
|
||||
|
||||
212
src/tools/common/notes.ts
Normal file
212
src/tools/common/notes.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Personal notes management tools
|
||||
*/
|
||||
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { mcpServer } from '../../server.js';
|
||||
import { database, Note } from '../../storage/database.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export function registerNoteTools(): void {
|
||||
// Create note
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'note_create',
|
||||
description: 'Create a new note',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Note title',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Note content',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Tags for categorization',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Optional ID for updating existing note',
|
||||
},
|
||||
},
|
||||
required: ['title', 'content'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const now = new Date().toISOString();
|
||||
const note: Note = {
|
||||
id: (args.id as string) || randomUUID(),
|
||||
title: args.title as string,
|
||||
content: args.content as string,
|
||||
tags: (args.tags as string[]) || [],
|
||||
createdAt: args.id
|
||||
? database.getNote(args.id)?.createdAt || now
|
||||
: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
database.saveNote(note);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Note "${note.title}" saved successfully with ID: ${note.id}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Search notes
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'note_search',
|
||||
description: 'Search notes by query',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (searches in title, content, and tags)',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const query = args.query as string;
|
||||
const notes = database.searchNotes(query);
|
||||
|
||||
if (notes.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No notes found matching "${query}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const results = notes
|
||||
.map(
|
||||
(n) =>
|
||||
`ID: ${n.id}\nTitle: ${n.title}\nTags: ${n.tags.join(', ')}\nCreated: ${new Date(n.createdAt).toLocaleDateString()}\n\nContent:\n${n.content}\n---`
|
||||
)
|
||||
.join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${notes.length} note(s):\n\n${results}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// List notes
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'note_list',
|
||||
description: 'List all notes',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of notes to return',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const notes = database.getNotes();
|
||||
const limit = args.limit as number | undefined;
|
||||
|
||||
// Sort by updated date (newest first)
|
||||
const sorted = notes.sort((a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
);
|
||||
|
||||
const limited = limit ? sorted.slice(0, limit) : sorted;
|
||||
|
||||
if (limited.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No notes found. Use note_create to create a note!',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const list = limited
|
||||
.map(
|
||||
(n) =>
|
||||
`📝 ${n.title}\nID: ${n.id}\nTags: ${n.tags.join(', ') || 'None'}\nUpdated: ${new Date(n.updatedAt).toLocaleDateString()}`
|
||||
)
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Total: ${notes.length} note(s)\n\n${list}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Delete note
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'note_delete',
|
||||
description: 'Delete a note by ID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the note to delete',
|
||||
},
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const id = args.id as string;
|
||||
const deleted = database.deleteNote(id);
|
||||
|
||||
if (deleted) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Note with ID ${id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Note with ID ${id} not found`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
168
src/tools/common/tasks.ts
Normal file
168
src/tools/common/tasks.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Task management tools
|
||||
*/
|
||||
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { mcpServer } from '../../server.js';
|
||||
import { database, Task } from '../../storage/database.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export function registerTaskTools(): void {
|
||||
// Add task
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'task_add',
|
||||
description: 'Add a new task',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Task title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Task description (optional)',
|
||||
},
|
||||
},
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const task: Task = {
|
||||
id: randomUUID(),
|
||||
title: args.title as string,
|
||||
description: args.description as string,
|
||||
completed: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
database.saveTask(task);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Task "${task.title}" added successfully with ID: ${task.id}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// List tasks
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'task_list',
|
||||
description: 'List tasks (optionally filter by completion status)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
completed: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by completion status (true for completed, false for pending, undefined for all)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const completed = args.completed as boolean | undefined;
|
||||
const tasks = database.getTasks(completed);
|
||||
|
||||
if (tasks.length === 0) {
|
||||
const statusText = completed === true ? 'completed' : completed === false ? 'pending' : '';
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No ${statusText} tasks found. Use task_add to add a task!`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
const sorted = tasks.sort((a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
const list = sorted
|
||||
.map((t) => {
|
||||
const status = t.completed ? '✅' : '⏳';
|
||||
return `${status} ${t.title}\nID: ${t.id}${t.description ? `\nDescription: ${t.description}` : ''}\nCreated: ${new Date(t.createdAt).toLocaleDateString()}${t.completed && t.completedAt ? `\nCompleted: ${new Date(t.completedAt).toLocaleDateString()}` : ''}`;
|
||||
})
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
const total = database.getTasks().length;
|
||||
const completedCount = database.getTasks(true).length;
|
||||
const pendingCount = database.getTasks(false).length;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Tasks (${tasks.length} shown, Total: ${total}, Completed: ${completedCount}, Pending: ${pendingCount}):\n\n${list}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Complete task
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'task_complete',
|
||||
description: 'Mark a task as completed',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'ID of the task to complete',
|
||||
},
|
||||
},
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const id = args.id as string;
|
||||
const task = database.getTask(id);
|
||||
|
||||
if (!task) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Task with ID ${id} not found`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (task.completed) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Task "${task.title}" is already completed`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
task.completed = true;
|
||||
task.completedAt = new Date().toISOString();
|
||||
database.saveTask(task);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Task "${task.title}" marked as completed! 🎉`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
296
src/tools/devops/nas.ts
Normal file
296
src/tools/devops/nas.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* NAS file management tools
|
||||
*/
|
||||
|
||||
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { configManager } from "../../storage/config.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
|
||||
export function registerNASTools(): void {
|
||||
// List NAS files
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "nas_list_files",
|
||||
description: "List files and directories on NAS",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to list (default: root)",
|
||||
default: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const nasConfig = configManager.getNASConfig();
|
||||
const path = (args.path as string) || "/";
|
||||
|
||||
if (!nasConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: NAS configuration not found. Please set NAS_HOST, NAS_USERNAME, and NAS_PASSWORD in environment variables.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Note: This is a placeholder implementation
|
||||
// In a real implementation, you would use appropriate libraries based on protocol
|
||||
// For SMB: smb2 or node-smb2
|
||||
// For FTP: basic-ftp
|
||||
// For SFTP: ssh2-sftp-client
|
||||
|
||||
const protocol = nasConfig.protocol || "smb";
|
||||
let result = "";
|
||||
|
||||
if (protocol === "smb") {
|
||||
result = `NAS File Listing (SMB protocol)\n`;
|
||||
result += `Host: ${nasConfig.host}\n`;
|
||||
result += `Path: ${path}\n\n`;
|
||||
result += `Note: SMB file listing requires additional libraries.\n`;
|
||||
result += `To implement, install: bun add smb2\n`;
|
||||
result += `Example files that would be listed:\n`;
|
||||
result += `- Documents/\n`;
|
||||
result += `- Media/\n`;
|
||||
result += `- Backups/`;
|
||||
} else if (protocol === "ftp" || protocol === "sftp") {
|
||||
result = `NAS File Listing (${protocol.toUpperCase()} protocol)\n`;
|
||||
result += `Host: ${nasConfig.host}\n`;
|
||||
result += `Path: ${path}\n\n`;
|
||||
result += `Note: ${protocol.toUpperCase()} file listing requires additional libraries.\n`;
|
||||
if (protocol === "ftp") {
|
||||
result += `To implement, install: bun add basic-ftp\n`;
|
||||
} else {
|
||||
result += `To implement, install: bun add ssh2-sftp-client\n`;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Listing NAS files at ${path}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error listing NAS files: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Upload file to NAS
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "nas_upload_file",
|
||||
description: "Upload a file to NAS",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
localPath: {
|
||||
type: "string",
|
||||
description: "Local file path to upload",
|
||||
},
|
||||
remotePath: {
|
||||
type: "string",
|
||||
description: "Remote path on NAS",
|
||||
},
|
||||
},
|
||||
required: ["localPath", "remotePath"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const nasConfig = configManager.getNASConfig();
|
||||
const localPath = args.localPath as string;
|
||||
const remotePath = args.remotePath as string;
|
||||
|
||||
if (!nasConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: NAS configuration not found. Please configure NAS settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Uploading ${localPath} to NAS ${remotePath}`);
|
||||
|
||||
// Placeholder implementation
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `File upload initiated:\nLocal: ${localPath}\nRemote: ${remotePath}\n\nNote: Full implementation requires protocol-specific libraries (smb2, basic-ftp, or ssh2-sftp-client).`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error uploading file: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Download file from NAS
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "nas_download_file",
|
||||
description: "Download a file from NAS",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
remotePath: {
|
||||
type: "string",
|
||||
description: "Remote file path on NAS",
|
||||
},
|
||||
localPath: {
|
||||
type: "string",
|
||||
description: "Local path to save the file",
|
||||
},
|
||||
},
|
||||
required: ["remotePath", "localPath"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const nasConfig = configManager.getNASConfig();
|
||||
const remotePath = args.remotePath as string;
|
||||
const localPath = args.localPath as string;
|
||||
|
||||
if (!nasConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: NAS configuration not found. Please configure NAS settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Downloading ${remotePath} from NAS to ${localPath}`);
|
||||
|
||||
// Placeholder implementation
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `File download initiated:\nRemote: ${remotePath}\nLocal: ${localPath}\n\nNote: Full implementation requires protocol-specific libraries.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error downloading file: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Search files on NAS
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "nas_search_files",
|
||||
description: "Search for files on NAS by name pattern",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: {
|
||||
type: "string",
|
||||
description: "File name pattern to search for",
|
||||
},
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Base path to search in (default: root)",
|
||||
default: "/",
|
||||
},
|
||||
},
|
||||
required: ["pattern"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const nasConfig = configManager.getNASConfig();
|
||||
const pattern = args.pattern as string;
|
||||
const path = (args.path as string) || "/";
|
||||
|
||||
if (!nasConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: NAS configuration not found. Please configure NAS settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Searching NAS for pattern: ${pattern} in ${path}`);
|
||||
|
||||
// Placeholder implementation
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Searching for files matching "${pattern}" in ${path}\n\nNote: Full implementation requires protocol-specific libraries and recursive directory traversal.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error searching files: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
204
src/tools/devops/router.ts
Normal file
204
src/tools/devops/router.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Soft router management tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { configManager } from "../../storage/config.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import axios from "axios";
|
||||
|
||||
export function registerRouterTools(): void {
|
||||
// Get router status
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "router_status",
|
||||
description: "Get soft router status and information",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const routerConfig = configManager.getRouterConfig();
|
||||
|
||||
if (!routerConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Router configuration not found. Please set ROUTER_HOST, ROUTER_USERNAME, and ROUTER_PASSWORD in environment variables.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Checking router status: ${routerConfig.host}`);
|
||||
|
||||
// Try to connect to router web interface (common ports: 80, 443, 8080)
|
||||
// Most routers have a status API endpoint
|
||||
const ports = [80, 443, 8080];
|
||||
let status = "";
|
||||
|
||||
for (const port of ports) {
|
||||
try {
|
||||
const protocol = port === 443 ? "https" : "http";
|
||||
await axios.get(`${protocol}://${routerConfig.host}:${port}/`, {
|
||||
timeout: 2000,
|
||||
auth:
|
||||
routerConfig.username && routerConfig.password
|
||||
? {
|
||||
username: routerConfig.username,
|
||||
password: routerConfig.password,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
status += `Router accessible on port ${port}\n`;
|
||||
break;
|
||||
} catch (error) {
|
||||
// Continue to next port
|
||||
}
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
status = `Router Status (${routerConfig.host}):\n\n`;
|
||||
status += `Note: Router status retrieval depends on router firmware.\n`;
|
||||
status += `Common router management interfaces:\n`;
|
||||
status += `- OpenWrt: http://${routerConfig.host}/cgi-bin/luci\n`;
|
||||
status += `- DD-WRT: http://${routerConfig.host}\n`;
|
||||
status += `- pfSense: https://${routerConfig.host}\n`;
|
||||
status += `\nFor full implementation, use router-specific API or SSH connection.`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: status,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting router status: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get traffic statistics
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "router_traffic",
|
||||
description: "Get router traffic statistics",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const routerConfig = configManager.getRouterConfig();
|
||||
|
||||
if (!routerConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Router configuration not found. Please configure router settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Getting router traffic stats: ${routerConfig.host}`);
|
||||
|
||||
// Placeholder - implementation depends on router firmware
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Router Traffic Statistics (${routerConfig.host}):\n\nNote: Traffic statistics retrieval depends on router firmware.\n\nFor OpenWrt, you can use:\n- SSH connection to execute: cat /proc/net/dev\n- Or access web interface: http://${routerConfig.host}/cgi-bin/luci/admin/network/bandwidth\n\nFor other routers, check their specific API documentation.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting traffic stats: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// List connected devices
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "router_devices",
|
||||
description: "List devices connected to the router",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const routerConfig = configManager.getRouterConfig();
|
||||
|
||||
if (!routerConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Router configuration not found. Please configure router settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Listing router devices: ${routerConfig.host}`);
|
||||
|
||||
// Placeholder - implementation depends on router firmware
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Connected Devices (${routerConfig.host}):\n\nNote: Device listing depends on router firmware.\n\nFor OpenWrt:\n- SSH: cat /proc/net/arp\n- Web: http://${routerConfig.host}/cgi-bin/luci/admin/network/dhcp\n\nFor other routers, check DHCP lease table or device list in web interface.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error listing devices: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
367
src/tools/devops/server.ts
Normal file
367
src/tools/devops/server.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Cloud server monitoring and management tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { configManager } from "../../storage/config.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { Client } from "ssh2";
|
||||
|
||||
export function registerServerTools(): void {
|
||||
// Get server status
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "server_status",
|
||||
description: "Get cloud server status (CPU, memory, disk usage)",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
const serverConfig = configManager.getServerConfig();
|
||||
|
||||
if (!serverConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Server configuration not found. Please set SERVER_HOST, SERVER_USERNAME, and SERVER_KEY_PATH in environment variables.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
|
||||
conn.on("ready", () => {
|
||||
logger.info("SSH connection established");
|
||||
|
||||
// Execute commands to get system status
|
||||
conn.exec(
|
||||
"echo 'CPU:'; top -l 1 | grep 'CPU usage' | awk '{print $3}'; echo 'Memory:'; vm_stat | head -n 5; echo 'Disk:'; df -h / | tail -n 1",
|
||||
(err: Error | undefined, stream: any) => {
|
||||
if (err) {
|
||||
conn.end();
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error executing command: ${err.message}`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
stream
|
||||
.on("close", () => {
|
||||
conn.end();
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Server Status (${serverConfig.host}):\n\n${
|
||||
output ||
|
||||
"Status retrieved successfully. Note: Command output may vary by OS."
|
||||
}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
output += data.toString();
|
||||
})
|
||||
.stderr.on("data", (data: Buffer) => {
|
||||
output += data.toString();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
conn.on("error", (err) => {
|
||||
logger.error("SSH connection error:", err);
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error connecting to server: ${err.message}\n\nMake sure:\n1. Server is accessible\n2. SSH key is configured correctly\n3. Server allows SSH connections`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
});
|
||||
|
||||
// Connect using key or password
|
||||
const connectOptions: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
privateKey?: string;
|
||||
password?: string;
|
||||
} = {
|
||||
host: serverConfig.host!,
|
||||
port: serverConfig.port || 22,
|
||||
username: serverConfig.username!,
|
||||
};
|
||||
|
||||
if (serverConfig.keyPath) {
|
||||
import("fs").then(({ readFileSync }) => {
|
||||
try {
|
||||
connectOptions.privateKey = readFileSync(serverConfig.keyPath!);
|
||||
conn.connect(connectOptions);
|
||||
} catch (error) {
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error reading SSH key: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
conn.connect(connectOptions);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting server status: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Deploy application
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "server_deploy",
|
||||
description: "Deploy application to cloud server",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
localPath: {
|
||||
type: "string",
|
||||
description: "Local path of the application to deploy",
|
||||
},
|
||||
remotePath: {
|
||||
type: "string",
|
||||
description: "Remote path on server",
|
||||
},
|
||||
command: {
|
||||
type: "string",
|
||||
description:
|
||||
'Optional command to run after deployment (e.g., "pm2 restart app")',
|
||||
},
|
||||
},
|
||||
required: ["localPath", "remotePath"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const serverConfig = configManager.getServerConfig();
|
||||
const localPath = args.localPath as string;
|
||||
const remotePath = args.remotePath as string;
|
||||
const command = args.command as string | undefined;
|
||||
|
||||
if (!serverConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Server configuration not found. Please configure server settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
`Deploying ${localPath} to ${serverConfig.host}:${remotePath}`
|
||||
);
|
||||
|
||||
// Placeholder - full implementation would use scp or sftp
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Deployment initiated:\nLocal: ${localPath}\nRemote: ${
|
||||
serverConfig.host
|
||||
}:${remotePath}\n${
|
||||
command ? `Command: ${command}` : ""
|
||||
}\n\nNote: Full deployment requires SCP/SFTP implementation. Consider using scp2 or ssh2-sftp-client libraries.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error deploying: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// View server logs
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "server_logs",
|
||||
description: "View server logs",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
logPath: {
|
||||
type: "string",
|
||||
description: "Path to log file (e.g., /var/log/app.log)",
|
||||
default: "/var/log/syslog",
|
||||
},
|
||||
lines: {
|
||||
type: "number",
|
||||
description: "Number of lines to retrieve",
|
||||
default: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const serverConfig = configManager.getServerConfig();
|
||||
const logPath = (args.logPath as string) || "/var/log/syslog";
|
||||
const lines = (args.lines as number) || 50;
|
||||
|
||||
if (!serverConfig.host) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Error: Server configuration not found. Please configure server settings.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return new Promise((resolve) => {
|
||||
const conn = new Client();
|
||||
|
||||
conn.on("ready", () => {
|
||||
conn.exec(
|
||||
`tail -n ${lines} ${logPath}`,
|
||||
(err: Error | undefined, stream: any) => {
|
||||
if (err) {
|
||||
conn.end();
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error reading logs: ${err.message}`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
stream
|
||||
.on("close", () => {
|
||||
conn.end();
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Server Logs (${logPath}, last ${lines} lines):\n\n${
|
||||
output || "No logs found or permission denied"
|
||||
}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
output += data.toString();
|
||||
})
|
||||
.stderr.on("data", (data: Buffer) => {
|
||||
output += data.toString();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
conn.on("error", (err: Error) => {
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error connecting to server: ${err.message}`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
});
|
||||
|
||||
const connectOptions: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
privateKey?: string;
|
||||
} = {
|
||||
host: serverConfig.host!,
|
||||
port: serverConfig.port || 22,
|
||||
username: serverConfig.username!,
|
||||
};
|
||||
|
||||
if (serverConfig.keyPath) {
|
||||
import("fs").then(({ readFileSync }) => {
|
||||
try {
|
||||
connectOptions.privateKey = readFileSync(serverConfig.keyPath!);
|
||||
conn.connect(connectOptions);
|
||||
} catch (error) {
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error reading SSH key: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
conn.connect(connectOptions);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error viewing logs: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
182
src/tools/family/baby.ts
Normal file
182
src/tools/family/baby.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Baby milestone and reminder tools
|
||||
*/
|
||||
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { mcpServer } from '../../server.js';
|
||||
import { database, BabyMilestone } from '../../storage/database.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export function registerBabyTools(): void {
|
||||
// Add baby milestone
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'baby_milestone_add',
|
||||
description: 'Record a baby milestone (e.g., first steps, first words)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Milestone title (e.g., "First steps", "First word")',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Detailed description of the milestone',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Date of the milestone (ISO format or YYYY-MM-DD)',
|
||||
},
|
||||
},
|
||||
required: ['title', 'description', 'date'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const now = new Date().toISOString();
|
||||
const milestone: BabyMilestone = {
|
||||
id: randomUUID(),
|
||||
title: args.title as string,
|
||||
description: args.description as string,
|
||||
date: args.date as string,
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
database.saveBabyMilestone(milestone);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Baby milestone "${milestone.title}" recorded successfully!\n\nDate: ${milestone.date}\nDescription: ${milestone.description}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// List baby milestones
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'baby_milestone_list',
|
||||
description: 'List all recorded baby milestones',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of milestones to return',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const milestones = database.getBabyMilestones();
|
||||
const limit = args.limit as number | undefined;
|
||||
|
||||
// Sort by date (newest first)
|
||||
const sorted = milestones.sort((a, b) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
const limited = limit ? sorted.slice(0, limit) : sorted;
|
||||
|
||||
if (limited.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No milestones recorded yet. Use baby_milestone_add to record milestones!',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const list = limited
|
||||
.map(
|
||||
(m) =>
|
||||
`📅 ${m.date}\n🎯 ${m.title}\n📝 ${m.description}\n---`
|
||||
)
|
||||
.join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Total: ${milestones.length} milestone(s) recorded\n\n${list}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Set baby reminder
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'baby_reminder_set',
|
||||
description: 'Set a reminder for baby-related tasks (vaccines, checkups, etc.)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Reminder title (e.g., "Vaccine", "Checkup", "Developmental screening")',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Reminder description',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Reminder date (ISO format or YYYY-MM-DD)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Reminder type (vaccine, checkup, screening, other)',
|
||||
default: 'other',
|
||||
},
|
||||
},
|
||||
required: ['title', 'date'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const title = args.title as string;
|
||||
const description = (args.description as string) || '';
|
||||
const date = args.date as string;
|
||||
const type = (args.type as string) || 'other';
|
||||
|
||||
// Save as a task in the database
|
||||
const { database: db } = await import('../../storage/database.js');
|
||||
const { Task } = await import('../../storage/database.js');
|
||||
|
||||
const task: Task = {
|
||||
id: randomUUID(),
|
||||
title: `[Baby] ${title}`,
|
||||
description: `Type: ${type}\n${description}`,
|
||||
completed: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
db.saveTask(task);
|
||||
|
||||
// Common baby reminders reference
|
||||
const commonReminders: Record<string, string> = {
|
||||
vaccine: 'Common vaccines: DTaP, MMR, Varicella, Hepatitis B',
|
||||
checkup: 'Regular checkups: 2 weeks, 1 month, 2 months, 4 months, 6 months, 9 months, 12 months, 15 months, 18 months, 2 years',
|
||||
screening: 'Developmental screenings: 9 months, 18 months, 24 months',
|
||||
};
|
||||
|
||||
const reminderInfo = commonReminders[type] || '';
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Baby reminder set successfully!\n\nTitle: ${title}\nDate: ${date}\nType: ${type}${description ? `\nDescription: ${description}` : ''}${reminderInfo ? `\n\nNote: ${reminderInfo}` : ''}\n\nReminder saved as a task. Use task_list to view all reminders.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
234
src/tools/family/math.ts
Normal file
234
src/tools/family/math.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Math teaching resource tools
|
||||
*/
|
||||
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { mcpServer } from '../../server.js';
|
||||
import { database, MathResource } from '../../storage/database.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export function registerMathTools(): void {
|
||||
// Search math resources
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'math_resource_search',
|
||||
description: 'Search for math teaching resources (worksheets, problems, tools)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
grade: {
|
||||
type: 'string',
|
||||
description: 'Grade level (e.g., "1st", "2nd", "elementary", "middle", "high")',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const query = args.query as string;
|
||||
const grade = args.grade as string | undefined;
|
||||
|
||||
const resources = database.searchMathResources(query, grade);
|
||||
|
||||
if (resources.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No math resources found matching "${query}"${grade ? ` for grade ${grade}` : ''}.\n\nYou can save resources using math_resource_save tool.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const results = resources
|
||||
.map(
|
||||
(r) =>
|
||||
`Title: ${r.title}\nGrade: ${r.grade || 'N/A'}\nDifficulty: ${r.difficulty || 'N/A'}\nTags: ${r.tags.join(', ')}\n\nContent:\n${r.content}\n---`
|
||||
)
|
||||
.join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${resources.length} math resource(s):\n\n${results}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Generate math problems
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'math_problem_generate',
|
||||
description: 'Generate math problems by grade and difficulty',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
grade: {
|
||||
type: 'string',
|
||||
description: 'Grade level (e.g., "1st", "2nd", "elementary", "middle", "high")',
|
||||
},
|
||||
difficulty: {
|
||||
type: 'string',
|
||||
description: 'Difficulty level (easy, medium, hard)',
|
||||
default: 'medium',
|
||||
},
|
||||
topic: {
|
||||
type: 'string',
|
||||
description: 'Math topic (e.g., "addition", "multiplication", "algebra", "geometry")',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of problems to generate',
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
required: ['grade'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const grade = args.grade as string;
|
||||
const difficulty = (args.difficulty as string) || 'medium';
|
||||
const topic = args.topic as string | undefined;
|
||||
const count = (args.count as number) || 5;
|
||||
|
||||
// Generate problems based on grade and difficulty
|
||||
const problems: string[] = [];
|
||||
|
||||
if (grade.includes('1st') || grade.includes('2nd') || grade === 'elementary') {
|
||||
// Elementary level
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (topic === 'addition' || !topic) {
|
||||
const a = Math.floor(Math.random() * 20) + 1;
|
||||
const b = Math.floor(Math.random() * 20) + 1;
|
||||
problems.push(`${a} + ${b} = ?`);
|
||||
} else if (topic === 'subtraction') {
|
||||
const a = Math.floor(Math.random() * 20) + 10;
|
||||
const b = Math.floor(Math.random() * a) + 1;
|
||||
problems.push(`${a} - ${b} = ?`);
|
||||
} else if (topic === 'multiplication') {
|
||||
const a = Math.floor(Math.random() * 10) + 1;
|
||||
const b = Math.floor(Math.random() * 10) + 1;
|
||||
problems.push(`${a} × ${b} = ?`);
|
||||
}
|
||||
}
|
||||
} else if (grade.includes('middle') || grade.includes('6th') || grade.includes('7th') || grade.includes('8th')) {
|
||||
// Middle school level
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (topic === 'algebra' || !topic) {
|
||||
const a = Math.floor(Math.random() * 10) + 1;
|
||||
const b = Math.floor(Math.random() * 20) - 10;
|
||||
const c = Math.floor(Math.random() * 20) - 10;
|
||||
problems.push(`Solve for x: ${a}x + ${b} = ${c}`);
|
||||
} else if (topic === 'fractions') {
|
||||
const num1 = Math.floor(Math.random() * 10) + 1;
|
||||
const den1 = Math.floor(Math.random() * 10) + 1;
|
||||
const num2 = Math.floor(Math.random() * 10) + 1;
|
||||
const den2 = Math.floor(Math.random() * 10) + 1;
|
||||
problems.push(`Add: ${num1}/${den1} + ${num2}/${den2} = ?`);
|
||||
} else if (topic === 'geometry') {
|
||||
const side = Math.floor(Math.random() * 10) + 1;
|
||||
problems.push(`Find the area of a square with side length ${side}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// High school level
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (topic === 'algebra' || !topic) {
|
||||
const a = Math.floor(Math.random() * 5) + 1;
|
||||
const b = Math.floor(Math.random() * 10) - 5;
|
||||
const c = Math.floor(Math.random() * 10) - 5;
|
||||
problems.push(`Solve: ${a}x² + ${b}x + ${c} = 0`);
|
||||
} else if (topic === 'geometry') {
|
||||
const r = Math.floor(Math.random() * 10) + 1;
|
||||
problems.push(`Find the area of a circle with radius ${r} (use π = 3.14)`);
|
||||
} else if (topic === 'trigonometry') {
|
||||
const angle = [30, 45, 60][Math.floor(Math.random() * 3)];
|
||||
problems.push(`Find sin(${angle}°)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const problemsText = problems.map((p, i) => `${i + 1}. ${p}`).join('\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Generated ${count} math problem(s) for ${grade} grade (${difficulty} difficulty)${topic ? ` - Topic: ${topic}` : ''}:\n\n${problemsText}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Save math resource
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'math_resource_save',
|
||||
description: 'Save a math teaching resource',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Title of the resource',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Content of the resource (worksheet, problem set, etc.)',
|
||||
},
|
||||
grade: {
|
||||
type: 'string',
|
||||
description: 'Grade level',
|
||||
},
|
||||
difficulty: {
|
||||
type: 'string',
|
||||
description: 'Difficulty level',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Tags for categorization',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Optional ID for updating existing resource',
|
||||
},
|
||||
},
|
||||
required: ['title', 'content'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const now = new Date().toISOString();
|
||||
const resource: MathResource = {
|
||||
id: (args.id as string) || randomUUID(),
|
||||
title: args.title as string,
|
||||
content: args.content as string,
|
||||
grade: args.grade as string | undefined,
|
||||
difficulty: args.difficulty as string | undefined,
|
||||
tags: (args.tags as string[]) || [],
|
||||
createdAt: now,
|
||||
};
|
||||
|
||||
database.saveMathResource(resource);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Math resource "${resource.title}" saved successfully with ID: ${resource.id}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
259
src/tools/hobbies/football.ts
Normal file
259
src/tools/hobbies/football.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Football information tools
|
||||
*/
|
||||
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { mcpServer } from '../../server.js';
|
||||
import { configManager } from '../../storage/config.js';
|
||||
import axios from 'axios';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
export function registerFootballTools(): void {
|
||||
// Get football matches
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'football_matches',
|
||||
description: 'Get upcoming or recent football matches',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
days: {
|
||||
type: 'number',
|
||||
description: 'Number of days ahead/behind to search (default: 7)',
|
||||
default: 7,
|
||||
},
|
||||
league: {
|
||||
type: 'string',
|
||||
description: 'League name (e.g., "Premier League", "La Liga", "Champions League")',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const days = (args.days as number) || 7;
|
||||
const league = args.league as string | undefined;
|
||||
const apiKey = configManager.getConfig().footballApiKey;
|
||||
|
||||
try {
|
||||
if (!apiKey) {
|
||||
// Return placeholder information
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Football Matches (${days} days):\n\nNote: To get real-time match data, set FOOTBALL_API_KEY in environment variables.\n\nFree APIs available:\n- football-data.org (requires free API key)\n- api-football.com\n\nExample upcoming matches:\n- Premier League: Manchester United vs Liverpool (Tomorrow 15:00)\n- La Liga: Real Madrid vs Barcelona (Saturday 20:00)\n- Champions League: Various matches this week`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Using football-data.org API (free tier)
|
||||
const today = new Date();
|
||||
const dateFrom = new Date(today);
|
||||
dateFrom.setDate(dateFrom.getDate() - days);
|
||||
const dateTo = new Date(today);
|
||||
dateTo.setDate(dateTo.getDate() + days);
|
||||
|
||||
const response = await axios.get(
|
||||
`https://api.football-data.org/v4/matches`,
|
||||
{
|
||||
headers: {
|
||||
'X-Auth-Token': apiKey,
|
||||
},
|
||||
params: {
|
||||
dateFrom: dateFrom.toISOString().split('T')[0],
|
||||
dateTo: dateTo.toISOString().split('T')[0],
|
||||
competitions: league ? undefined : undefined, // Would need competition IDs
|
||||
},
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const matches = response.data.matches || [];
|
||||
if (matches.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No matches found for the specified period.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const matchesText = matches
|
||||
.slice(0, 20) // Limit to 20 matches
|
||||
.map((match: any) => {
|
||||
const home = match.homeTeam?.name || 'TBD';
|
||||
const away = match.awayTeam?.name || 'TBD';
|
||||
const date = new Date(match.utcDate).toLocaleString();
|
||||
const status = match.status || 'SCHEDULED';
|
||||
return `${home} vs ${away}\nDate: ${date}\nStatus: ${status}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Football Matches (${matches.length} found):\n\n${matchesText}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching football matches:', error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching matches: ${error instanceof Error ? error.message : String(error)}\n\nNote: Make sure FOOTBALL_API_KEY is set correctly.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get team information
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'football_team_info',
|
||||
description: 'Get information about a football team',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
team: {
|
||||
type: 'string',
|
||||
description: 'Team name',
|
||||
},
|
||||
},
|
||||
required: ['team'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const team = args.team as string;
|
||||
const apiKey = configManager.getConfig().footballApiKey;
|
||||
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Team Information: ${team}\n\nNote: To get real-time team data, set FOOTBALL_API_KEY in environment variables.\n\nYou can use football-data.org API (free tier available).`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Search for team
|
||||
const searchResponse = await axios.get(
|
||||
`https://api.football-data.org/v4/teams`,
|
||||
{
|
||||
headers: {
|
||||
'X-Auth-Token': apiKey,
|
||||
},
|
||||
params: {
|
||||
name: team,
|
||||
},
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
const teams = searchResponse.data.teams || [];
|
||||
if (teams.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Team "${team}" not found.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const teamData = teams[0];
|
||||
const info = `Team: ${teamData.name}\nFounded: ${teamData.founded || 'N/A'}\nVenue: ${teamData.venue || 'N/A'}\nWebsite: ${teamData.website || 'N/A'}`;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: info,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching team info:', error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching team information: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get league standings
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'football_standings',
|
||||
description: 'Get league standings/table',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
league: {
|
||||
type: 'string',
|
||||
description: 'League name or competition ID',
|
||||
},
|
||||
},
|
||||
required: ['league'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const league = args.league as string;
|
||||
const apiKey = configManager.getConfig().footballApiKey;
|
||||
|
||||
try {
|
||||
if (!apiKey) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `League Standings: ${league}\n\nNote: To get real-time standings, set FOOTBALL_API_KEY in environment variables.\n\nCommon leagues:\n- Premier League (2021)\n- La Liga (2014)\n- Bundesliga (2002)\n- Serie A (2019)\n- Ligue 1 (2015)\n- Champions League (2001)`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Note: This would require competition ID mapping
|
||||
// For now, return placeholder
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `League Standings for ${league}:\n\nNote: Full implementation requires competition ID mapping.\n\nYou can find competition IDs at:\nhttps://api.football-data.org/v4/competitions`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching standings:', error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching standings: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
315
src/tools/hobbies/games.ts
Normal file
315
src/tools/hobbies/games.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Game information tools
|
||||
*/
|
||||
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { mcpServer } from '../../server.js';
|
||||
import { database, GameWishlist } from '../../storage/database.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import axios from 'axios';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
export function registerGameTools(): void {
|
||||
// Get game information
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'game_info',
|
||||
description: 'Get information about a video game',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Game name',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const name = args.name as string;
|
||||
|
||||
try {
|
||||
// Using RAWG.io API (free, no key required for basic usage)
|
||||
const response = await axios.get('https://api.rawg.io/api/games', {
|
||||
params: {
|
||||
search: name,
|
||||
page_size: 1,
|
||||
},
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const games = response.data.results || [];
|
||||
if (games.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Game "${name}" not found.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const game = games[0];
|
||||
const info = `Game: ${game.name}\nReleased: ${game.released || 'TBA'}\nRating: ${game.rating || 'N/A'}/5\nMetacritic: ${game.metacritic || 'N/A'}\nPlatforms: ${game.platforms?.map((p: any) => p.platform.name).join(', ') || 'N/A'}\nGenres: ${game.genres?.map((g: any) => g.name).join(', ') || 'N/A'}\nWebsite: ${game.website || 'N/A'}`;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: info,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching game info:', error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching game information: ${error instanceof Error ? error.message : String(error)}\n\nGame: ${name}\n\nNote: Using RAWG.io API for game information.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get game deals
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'game_deals',
|
||||
description: 'Get game deals and discounts',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
platform: {
|
||||
type: 'string',
|
||||
description: 'Platform (steam, epic, psn, xbox, switch)',
|
||||
},
|
||||
maxPrice: {
|
||||
type: 'number',
|
||||
description: 'Maximum price filter',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const platform = args.platform as string | undefined;
|
||||
const maxPrice = args.maxPrice as number | undefined;
|
||||
|
||||
try {
|
||||
// Using CheapShark API (free, no key required)
|
||||
const params: Record<string, string> = {};
|
||||
if (platform) {
|
||||
params.storeID = {
|
||||
steam: '1',
|
||||
epic: '25',
|
||||
psn: '7',
|
||||
xbox: '2',
|
||||
switch: '6',
|
||||
}[platform.toLowerCase()] || '1';
|
||||
}
|
||||
if (maxPrice) {
|
||||
params.upperPrice = maxPrice.toString();
|
||||
}
|
||||
|
||||
const response = await axios.get('https://www.cheapshark.com/api/1.0/deals', {
|
||||
params: {
|
||||
...params,
|
||||
pageSize: 20,
|
||||
sortBy: 'Deal Rating',
|
||||
},
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const deals = response.data || [];
|
||||
if (deals.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No deals found matching your criteria.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const dealsText = deals
|
||||
.map((deal: any) => {
|
||||
return `${deal.title}\nPrice: $${deal.salePrice} (Was: $${deal.normalPrice})\nSavings: ${deal.savings}%\nStore: ${deal.storeID}\nLink: https://www.cheapshark.com/redirect?dealID=${deal.dealID}`;
|
||||
})
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Game Deals (${deals.length} found):\n\n${dealsText}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error fetching game deals:', error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error fetching game deals: ${error instanceof Error ? error.message : String(error)}\n\nNote: Using CheapShark API for game deals.`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Manage game wishlist
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: 'game_wishlist',
|
||||
description: 'Add, list, or remove games from wishlist',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action: "add", "list", or "remove"',
|
||||
enum: ['add', 'list', 'remove'],
|
||||
},
|
||||
gameName: {
|
||||
type: 'string',
|
||||
description: 'Game name (required for add/remove)',
|
||||
},
|
||||
platform: {
|
||||
type: 'string',
|
||||
description: 'Platform (optional)',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Notes about the game (optional)',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Game ID (required for remove)',
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const action = args.action as string;
|
||||
|
||||
if (action === 'add') {
|
||||
const gameName = args.gameName as string;
|
||||
if (!gameName) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: gameName is required for add action',
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const game: GameWishlist = {
|
||||
id: randomUUID(),
|
||||
gameName,
|
||||
platform: args.platform as string | undefined,
|
||||
notes: args.notes as string | undefined,
|
||||
addedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
database.saveGameWishlist(game);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Game "${gameName}" added to wishlist with ID: ${game.id}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (action === 'list') {
|
||||
const games = database.getGameWishlist();
|
||||
|
||||
if (games.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Your wishlist is empty. Use game_wishlist with action="add" to add games.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const list = games
|
||||
.map(
|
||||
(g) =>
|
||||
`🎮 ${g.gameName}${g.platform ? ` (${g.platform})` : ''}\nID: ${g.id}${g.notes ? `\nNotes: ${g.notes}` : ''}\nAdded: ${new Date(g.addedAt).toLocaleDateString()}`
|
||||
)
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Your Game Wishlist (${games.length} games):\n\n${list}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (action === 'remove') {
|
||||
const id = args.id as string;
|
||||
if (!id) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Error: id is required for remove action',
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const deleted = database.deleteGameWishlist(id);
|
||||
|
||||
if (deleted) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Game with ID ${id} removed from wishlist`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Game with ID ${id} not found in wishlist`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Invalid action: ${action}. Use "add", "list", or "remove"`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
233
src/tools/programming/codeReview.ts
Normal file
233
src/tools/programming/codeReview.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Code review and optimization tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
|
||||
export function registerCodeReviewTools(): void {
|
||||
// Code review
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "code_review",
|
||||
description: "Review code and provide suggestions for improvement",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
description: "The code to review",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "Programming language",
|
||||
},
|
||||
},
|
||||
required: ["code", "language"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const code = args.code as string;
|
||||
const language = args.language as string;
|
||||
|
||||
// Basic code review analysis
|
||||
const issues: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// Check for common issues
|
||||
if (code.includes("any")) {
|
||||
issues.push('Found "any" type - consider using specific types');
|
||||
}
|
||||
|
||||
if (code.includes("console.log")) {
|
||||
suggestions.push(
|
||||
"Consider removing console.log statements in production code"
|
||||
);
|
||||
}
|
||||
|
||||
if (code.includes("var ")) {
|
||||
issues.push('Found "var" - prefer "let" or "const"');
|
||||
}
|
||||
|
||||
if (code.match(/function\s+\w+\s*\(/)) {
|
||||
suggestions.push("Consider using arrow functions for consistency");
|
||||
}
|
||||
|
||||
// Check for error handling
|
||||
if (
|
||||
!code.includes("try") &&
|
||||
!code.includes("catch") &&
|
||||
code.includes("async")
|
||||
) {
|
||||
suggestions.push("Consider adding error handling for async operations");
|
||||
}
|
||||
|
||||
// Check for TypeScript/Vue specific
|
||||
if (language === "typescript" || language === "ts") {
|
||||
if (code.includes("@ts-ignore") || code.includes("@ts-nocheck")) {
|
||||
issues.push(
|
||||
"Found TypeScript ignore comments - try to fix the underlying issues"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (language === "vue" || language === "vue3") {
|
||||
if (code.includes("Options API") && code.includes("setup()")) {
|
||||
suggestions.push(
|
||||
"Consider using Composition API with <script setup> for better TypeScript support"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let response = `Code Review for ${language} code:\n\n`;
|
||||
|
||||
if (issues.length > 0) {
|
||||
response += `Issues Found:\n`;
|
||||
issues.forEach((issue, i) => {
|
||||
response += `${i + 1}. ${issue}\n`;
|
||||
});
|
||||
response += `\n`;
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
response += `Suggestions:\n`;
|
||||
suggestions.forEach((suggestion, i) => {
|
||||
response += `${i + 1}. ${suggestion}\n`;
|
||||
});
|
||||
response += `\n`;
|
||||
}
|
||||
|
||||
if (issues.length === 0 && suggestions.length === 0) {
|
||||
response += `No obvious issues found. Code looks good!\n`;
|
||||
response += `General best practices:\n`;
|
||||
response += `- Use TypeScript types strictly\n`;
|
||||
response += `- Add error handling\n`;
|
||||
response += `- Follow consistent code style\n`;
|
||||
response += `- Add comments for complex logic\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Code optimization
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "code_optimize",
|
||||
description: "Provide code optimization suggestions",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
code: {
|
||||
type: "string",
|
||||
description: "The code to optimize",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description: "Programming language",
|
||||
},
|
||||
},
|
||||
required: ["code", "language"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const code = args.code as string;
|
||||
const language = args.language as string;
|
||||
|
||||
const optimizations: string[] = [];
|
||||
|
||||
// Performance optimizations
|
||||
if (
|
||||
code.includes(".map(") &&
|
||||
code.includes(".filter(") &&
|
||||
code.includes(".map(")
|
||||
) {
|
||||
optimizations.push(
|
||||
"Consider combining multiple array operations to reduce iterations"
|
||||
);
|
||||
}
|
||||
|
||||
if (code.match(/for\s*\(\s*let\s+\w+\s*=\s*0/)) {
|
||||
optimizations.push(
|
||||
"Consider using for...of or array methods for better readability"
|
||||
);
|
||||
}
|
||||
|
||||
if (code.includes("==")) {
|
||||
optimizations.push("Use === instead of == for strict equality checks");
|
||||
}
|
||||
|
||||
// Vue-specific optimizations
|
||||
if (language === "vue" || language === "vue3") {
|
||||
if (code.includes("v-for") && !code.includes(":key")) {
|
||||
optimizations.push(
|
||||
"Add :key attribute to v-for for better performance"
|
||||
);
|
||||
}
|
||||
if (code.includes("computed") && code.includes("watch")) {
|
||||
optimizations.push(
|
||||
"Consider using computed instead of watch when possible"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TypeScript optimizations
|
||||
if (language === "typescript" || language === "ts") {
|
||||
if (code.includes("interface") && code.includes("type")) {
|
||||
optimizations.push(
|
||||
'Consider using "type" for unions/intersections, "interface" for object shapes'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Bun/Node optimizations
|
||||
if (language === "typescript" || language === "javascript") {
|
||||
if (code.includes("require(")) {
|
||||
optimizations.push(
|
||||
"Consider using ES modules (import/export) instead of require"
|
||||
);
|
||||
}
|
||||
if (
|
||||
(code.includes("Promise.all") &&
|
||||
code.match(/await\s+\w+\(\)/g)?.length) ||
|
||||
0 > 3
|
||||
) {
|
||||
optimizations.push(
|
||||
"Consider using Promise.all() for parallel async operations"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let response = `Code Optimization Suggestions for ${language}:\n\n`;
|
||||
|
||||
if (optimizations.length > 0) {
|
||||
optimizations.forEach((opt, i) => {
|
||||
response += `${i + 1}. ${opt}\n`;
|
||||
});
|
||||
} else {
|
||||
response += `No obvious optimization opportunities found.\n\n`;
|
||||
response += `General optimization tips:\n`;
|
||||
response += `- Use appropriate data structures\n`;
|
||||
response += `- Minimize re-renders (for UI frameworks)\n`;
|
||||
response += `- Use memoization for expensive computations\n`;
|
||||
response += `- Avoid unnecessary object creation in loops\n`;
|
||||
response += `- Use async/await properly with Promise.all for parallel operations\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
228
src/tools/programming/codeSnippet.ts
Normal file
228
src/tools/programming/codeSnippet.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Code snippet management tools
|
||||
*/
|
||||
|
||||
import { database, CodeSnippet } from "../../storage/database.js";
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export function registerCodeSnippetTools(): void {
|
||||
// Save code snippet
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "code_snippet_save",
|
||||
description:
|
||||
"Save a code snippet with title, code, language, tags, and optional category",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: {
|
||||
type: "string",
|
||||
description: "Title of the code snippet",
|
||||
},
|
||||
code: {
|
||||
type: "string",
|
||||
description: "The code content",
|
||||
},
|
||||
language: {
|
||||
type: "string",
|
||||
description:
|
||||
"Programming language (e.g., typescript, javascript, vue)",
|
||||
},
|
||||
tags: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Tags for categorization",
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
description: "Optional category (e.g., utils, components, api)",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
description: "Optional ID for updating existing snippet",
|
||||
},
|
||||
},
|
||||
required: ["title", "code", "language", "tags"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const now = new Date().toISOString();
|
||||
const snippet: CodeSnippet = {
|
||||
id: (args.id as string) || randomUUID(),
|
||||
title: args.title as string,
|
||||
code: args.code as string,
|
||||
language: args.language as string,
|
||||
tags: args.tags as string[],
|
||||
category: args.category as string,
|
||||
createdAt: args.id
|
||||
? database.getCodeSnippet(args.id as string)?.createdAt || now
|
||||
: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
database.saveCodeSnippet(snippet);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Code snippet "${snippet.title}" saved successfully with ID: ${snippet.id}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Search code snippets
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "code_snippet_search",
|
||||
description: "Search code snippets by query and optional tags",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query (searches in title, code, and language)",
|
||||
},
|
||||
tags: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Optional tags to filter by",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const query = args.query as string;
|
||||
const tags = args.tags as string[] | undefined;
|
||||
const snippets = database.searchCodeSnippets(query, tags);
|
||||
|
||||
if (snippets.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No code snippets found matching "${query}"`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const results = snippets
|
||||
.map(
|
||||
(s) =>
|
||||
`ID: ${s.id}\nTitle: ${s.title}\nLanguage: ${
|
||||
s.language
|
||||
}\nTags: ${s.tags.join(", ")}\nCategory: ${
|
||||
s.category || "N/A"
|
||||
}\n\nCode:\n\`\`\`${s.language}\n${s.code}\n\`\`\`\n---`
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${snippets.length} code snippet(s):\n\n${results}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// List all code snippets
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "code_snippet_list",
|
||||
description: "List all saved code snippets",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Maximum number of snippets to return",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const snippets = database.getCodeSnippets();
|
||||
const limit = args.limit as number | undefined;
|
||||
const limited = limit ? snippets.slice(0, limit) : snippets;
|
||||
|
||||
if (limited.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "No code snippets saved yet",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const list = limited
|
||||
.map(
|
||||
(s) =>
|
||||
`- ${s.title} (${s.language}) - ID: ${s.id}\n Tags: ${s.tags.join(
|
||||
", "
|
||||
)}`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Total: ${snippets.length} snippet(s)\n\n${list}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Delete code snippet
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "code_snippet_delete",
|
||||
description: "Delete a code snippet by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description: "ID of the code snippet to delete",
|
||||
},
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const id = args.id as string;
|
||||
const deleted = database.deleteCodeSnippet(id);
|
||||
|
||||
if (deleted) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Code snippet with ID ${id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Code snippet with ID ${id} not found`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
185
src/tools/programming/docs.ts
Normal file
185
src/tools/programming/docs.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Technical documentation query tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
|
||||
const DOCS_LINKS = {
|
||||
typescript: {
|
||||
official: "https://www.typescriptlang.org/docs/",
|
||||
handbook: "https://www.typescriptlang.org/docs/handbook/intro.html",
|
||||
api: "https://www.typescriptlang.org/docs/handbook/utility-types.html",
|
||||
},
|
||||
vue3: {
|
||||
official: "https://vuejs.org/",
|
||||
guide: "https://vuejs.org/guide/",
|
||||
api: "https://vuejs.org/api/",
|
||||
migration: "https://vuejs.org/guide/extras/migration-build.html",
|
||||
},
|
||||
bun: {
|
||||
official: "https://bun.sh/docs",
|
||||
runtime: "https://bun.sh/docs/runtime",
|
||||
api: "https://bun.sh/docs/api",
|
||||
test: "https://bun.sh/docs/test",
|
||||
},
|
||||
};
|
||||
|
||||
export function registerDocsTools(): void {
|
||||
// TypeScript docs
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "docs_typescript",
|
||||
description: "Get TypeScript documentation links and information",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
topic: {
|
||||
type: "string",
|
||||
description:
|
||||
'Specific topic to search for (e.g., "types", "interfaces", "generics")',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const topic = args.topic as string | undefined;
|
||||
const links = DOCS_LINKS.typescript;
|
||||
|
||||
let response = `TypeScript Documentation:\n\n`;
|
||||
response += `Official Docs: ${links.official}\n`;
|
||||
response += `Handbook: ${links.handbook}\n`;
|
||||
response += `Utility Types API: ${links.api}\n\n`;
|
||||
|
||||
if (topic) {
|
||||
response += `Searching for: ${topic}\n\n`;
|
||||
response += `Common TypeScript topics:\n`;
|
||||
response += `- Types & Interfaces: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html\n`;
|
||||
response += `- Generics: https://www.typescriptlang.org/docs/handbook/2/generics.html\n`;
|
||||
response += `- Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html\n`;
|
||||
response += `- Modules: https://www.typescriptlang.org/docs/handbook/2/modules.html\n`;
|
||||
response += `- Type Guards: https://www.typescriptlang.org/docs/handbook/2/narrowing.html\n`;
|
||||
} else {
|
||||
response += `Quick Links:\n`;
|
||||
response += `- Basic Types: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html\n`;
|
||||
response += `- Advanced Types: https://www.typescriptlang.org/docs/handbook/2/types-from-types.html\n`;
|
||||
response += `- Utility Types: https://www.typescriptlang.org/docs/handbook/utility-types.html\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Vue3 docs
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "docs_vue3",
|
||||
description: "Get Vue 3 documentation links and information",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
topic: {
|
||||
type: "string",
|
||||
description:
|
||||
'Specific topic (e.g., "composition", "reactivity", "components")',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const topic = args.topic as string | undefined;
|
||||
const links = DOCS_LINKS.vue3;
|
||||
|
||||
let response = `Vue 3 Documentation:\n\n`;
|
||||
response += `Official Site: ${links.official}\n`;
|
||||
response += `Guide: ${links.guide}\n`;
|
||||
response += `API Reference: ${links.api}\n\n`;
|
||||
|
||||
if (topic) {
|
||||
response += `Searching for: ${topic}\n\n`;
|
||||
response += `Common Vue 3 topics:\n`;
|
||||
response += `- Composition API: https://vuejs.org/guide/extras/composition-api-faq.html\n`;
|
||||
response += `- Reactivity: https://vuejs.org/guide/essentials/reactivity-fundamentals.html\n`;
|
||||
response += `- Components: https://vuejs.org/guide/essentials/component-basics.html\n`;
|
||||
response += `- Props: https://vuejs.org/guide/components/props.html\n`;
|
||||
response += `- Events: https://vuejs.org/guide/components/events.html\n`;
|
||||
response += `- Lifecycle: https://vuejs.org/guide/essentials/lifecycle.html\n`;
|
||||
} else {
|
||||
response += `Quick Links:\n`;
|
||||
response += `- Getting Started: https://vuejs.org/guide/quick-start.html\n`;
|
||||
response += `- Composition API: https://vuejs.org/guide/extras/composition-api-faq.html\n`;
|
||||
response += `- Reactivity Fundamentals: https://vuejs.org/guide/essentials/reactivity-fundamentals.html\n`;
|
||||
response += `- Components Basics: https://vuejs.org/guide/essentials/component-basics.html\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Bun docs
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "docs_bun",
|
||||
description: "Get Bun documentation links and information",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
topic: {
|
||||
type: "string",
|
||||
description:
|
||||
'Specific topic (e.g., "runtime", "api", "test", "bundler")',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const topic = args.topic as string | undefined;
|
||||
const links = DOCS_LINKS.bun;
|
||||
|
||||
let response = `Bun Documentation:\n\n`;
|
||||
response += `Official Docs: ${links.official}\n`;
|
||||
response += `Runtime: ${links.runtime}\n`;
|
||||
response += `API Reference: ${links.api}\n`;
|
||||
response += `Testing: ${links.test}\n\n`;
|
||||
|
||||
if (topic) {
|
||||
response += `Searching for: ${topic}\n\n`;
|
||||
response += `Common Bun topics:\n`;
|
||||
response += `- Runtime: https://bun.sh/docs/runtime\n`;
|
||||
response += `- File System: https://bun.sh/docs/api/file-io\n`;
|
||||
response += `- HTTP Server: https://bun.sh/docs/api/http\n`;
|
||||
response += `- SQLite: https://bun.sh/docs/api/sqlite\n`;
|
||||
response += `- Testing: https://bun.sh/docs/test\n`;
|
||||
response += `- Bundler: https://bun.sh/docs/bundler\n`;
|
||||
} else {
|
||||
response += `Quick Links:\n`;
|
||||
response += `- Getting Started: https://bun.sh/docs/installation\n`;
|
||||
response += `- Runtime API: https://bun.sh/docs/runtime\n`;
|
||||
response += `- HTTP Server: https://bun.sh/docs/api/http\n`;
|
||||
response += `- File System: https://bun.sh/docs/api/file-io\n`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
622
src/tools/programming/projectTemplate.ts
Normal file
622
src/tools/programming/projectTemplate.ts
Normal file
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* Project template generation tools
|
||||
*/
|
||||
|
||||
import { mcpServer } from "../../server.js";
|
||||
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export function registerProjectTemplateTools(): void {
|
||||
// Create Vite + Vue3 project
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "project_template_create",
|
||||
description:
|
||||
"Create a new Vite + Vue3 + TypeScript project with optional features",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Project name",
|
||||
},
|
||||
path: {
|
||||
type: "string",
|
||||
description:
|
||||
"Path where to create the project (default: current directory)",
|
||||
},
|
||||
usePinia: {
|
||||
type: "boolean",
|
||||
description: "Include Pinia for state management",
|
||||
default: false,
|
||||
},
|
||||
useRouter: {
|
||||
type: "boolean",
|
||||
description: "Include Vue Router",
|
||||
default: false,
|
||||
},
|
||||
useTailwind: {
|
||||
type: "boolean",
|
||||
description: "Include Tailwind CSS",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const name = args.name as string;
|
||||
const basePath = (args.path as string) || process.cwd();
|
||||
const projectPath = join(basePath, name);
|
||||
const usePinia = (args.usePinia as boolean) || false;
|
||||
const useRouter = (args.useRouter as boolean) || false;
|
||||
const useTailwind = (args.useTailwind as boolean) || false;
|
||||
|
||||
if (existsSync(projectPath)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Directory ${projectPath} already exists`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(projectPath, { recursive: true });
|
||||
mkdirSync(join(projectPath, "src"), { recursive: true });
|
||||
mkdirSync(join(projectPath, "src", "components"), { recursive: true });
|
||||
|
||||
// package.json
|
||||
const dependencies: Record<string, string> = {
|
||||
vue: "^3.4.0",
|
||||
};
|
||||
const devDependencies: Record<string, string> = {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
typescript: "^5.6.3",
|
||||
vite: "^5.4.0",
|
||||
"vue-tsc": "^2.0.0",
|
||||
};
|
||||
|
||||
if (usePinia) {
|
||||
dependencies.pinia = "^2.1.7";
|
||||
}
|
||||
if (useRouter) {
|
||||
dependencies["vue-router"] = "^4.3.0";
|
||||
}
|
||||
if (useTailwind) {
|
||||
devDependencies.tailwindcss = "^3.4.0";
|
||||
devDependencies.autoprefixer = "^10.4.18";
|
||||
devDependencies.postcss = "^8.4.35";
|
||||
}
|
||||
|
||||
const packageJson = {
|
||||
name,
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "vue-tsc && vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
dependencies,
|
||||
devDependencies,
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "package.json"),
|
||||
JSON.stringify(packageJson, null, 2)
|
||||
);
|
||||
|
||||
// tsconfig.json
|
||||
const tsconfig = {
|
||||
compilerOptions: {
|
||||
target: "ES2020",
|
||||
useDefineForClassFields: true,
|
||||
module: "ESNext",
|
||||
lib: ["ES2020", "DOM", "DOM.Iterable"],
|
||||
skipLibCheck: true,
|
||||
moduleResolution: "bundler",
|
||||
allowImportingTsExtensions: true,
|
||||
resolveJsonModule: true,
|
||||
isolatedModules: true,
|
||||
noEmit: true,
|
||||
jsx: "preserve",
|
||||
strict: true,
|
||||
noUnusedLocals: true,
|
||||
noUnusedParameters: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
},
|
||||
include: ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
references: [{ path: "./tsconfig.node.json" }],
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "tsconfig.json"),
|
||||
JSON.stringify(tsconfig, null, 2)
|
||||
);
|
||||
|
||||
// vite.config.ts
|
||||
let viteConfig = `import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
||||
`;
|
||||
|
||||
writeFileSync(join(projectPath, "vite.config.ts"), viteConfig);
|
||||
|
||||
// index.html
|
||||
const indexHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${name}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
writeFileSync(join(projectPath, "index.html"), indexHtml);
|
||||
|
||||
// src/main.ts
|
||||
let mainTs = `import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
`;
|
||||
|
||||
if (usePinia) {
|
||||
mainTs += `import { createPinia } from 'pinia'\n\n`;
|
||||
}
|
||||
if (useRouter) {
|
||||
mainTs += `import router from './router'\n\n`;
|
||||
}
|
||||
|
||||
mainTs += `const app = createApp(App)\n`;
|
||||
|
||||
if (usePinia) {
|
||||
mainTs += `app.use(createPinia())\n`;
|
||||
}
|
||||
if (useRouter) {
|
||||
mainTs += `app.use(router)\n`;
|
||||
}
|
||||
|
||||
mainTs += `app.mount('#app')\n`;
|
||||
|
||||
writeFileSync(join(projectPath, "src", "main.ts"), mainTs);
|
||||
|
||||
// src/App.vue
|
||||
const appVue = `<script setup lang="ts">
|
||||
// Your app logic here
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<h1>Welcome to ${name}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
writeFileSync(join(projectPath, "src", "App.vue"), appVue);
|
||||
|
||||
// src/style.css
|
||||
writeFileSync(join(projectPath, "src", "style.css"), "");
|
||||
|
||||
// .gitignore
|
||||
const gitignore = `node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.DS_Store
|
||||
`;
|
||||
|
||||
writeFileSync(join(projectPath, ".gitignore"), gitignore);
|
||||
|
||||
// README.md
|
||||
const readme = `# ${name}
|
||||
|
||||
A Vue 3 + TypeScript + Vite project.
|
||||
|
||||
## Setup
|
||||
|
||||
\`\`\`bash
|
||||
bun install
|
||||
\`\`\`
|
||||
|
||||
## Development
|
||||
|
||||
\`\`\`bash
|
||||
bun run dev
|
||||
\`\`\`
|
||||
|
||||
## Build
|
||||
|
||||
\`\`\`bash
|
||||
bun run build
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
writeFileSync(join(projectPath, "README.md"), readme);
|
||||
|
||||
const features = [];
|
||||
if (usePinia) features.push("Pinia");
|
||||
if (useRouter) features.push("Vue Router");
|
||||
if (useTailwind) features.push("Tailwind CSS");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Project "${name}" created successfully at ${projectPath}\n\nFeatures: ${
|
||||
features.length > 0 ? features.join(", ") : "Basic setup"
|
||||
}\n\nNext steps:\n1. cd ${name}\n2. bun install\n3. bun run dev`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating project: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Create fullstack project
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "project_template_create_fullstack",
|
||||
description:
|
||||
"Create a fullstack project with Vite+Vue3 frontend and Bun backend",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Project name",
|
||||
},
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path where to create the project",
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
async (args) => {
|
||||
const name = args.name as string;
|
||||
const basePath = (args.path as string) || process.cwd();
|
||||
const projectPath = join(basePath, name);
|
||||
|
||||
if (existsSync(projectPath)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Directory ${projectPath} already exists`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(projectPath, { recursive: true });
|
||||
mkdirSync(join(projectPath, "frontend"), { recursive: true });
|
||||
mkdirSync(join(projectPath, "backend"), { recursive: true });
|
||||
mkdirSync(join(projectPath, "backend", "src"), { recursive: true });
|
||||
|
||||
// Root package.json
|
||||
const rootPackageJson = {
|
||||
name: `${name}-root`,
|
||||
version: "0.1.0",
|
||||
private: true,
|
||||
scripts: {
|
||||
dev: 'concurrently "bun run --cwd frontend dev" "bun run --cwd backend dev"',
|
||||
"dev:frontend": "bun run --cwd frontend dev",
|
||||
"dev:backend": "bun run --cwd backend dev",
|
||||
},
|
||||
devDependencies: {
|
||||
concurrently: "^8.2.2",
|
||||
},
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "package.json"),
|
||||
JSON.stringify(rootPackageJson, null, 2)
|
||||
);
|
||||
|
||||
// Frontend package.json
|
||||
const frontendPackageJson = {
|
||||
name: `${name}-frontend`,
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "vue-tsc && vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
dependencies: {
|
||||
vue: "^3.4.0",
|
||||
axios: "^1.7.7",
|
||||
},
|
||||
devDependencies: {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
typescript: "^5.6.3",
|
||||
vite: "^5.4.0",
|
||||
"vue-tsc": "^2.0.0",
|
||||
},
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "frontend", "package.json"),
|
||||
JSON.stringify(frontendPackageJson, null, 2)
|
||||
);
|
||||
|
||||
// Backend package.json
|
||||
const backendPackageJson = {
|
||||
name: `${name}-backend`,
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
scripts: {
|
||||
dev: "bun run src/index.ts",
|
||||
build: "bun build src/index.ts --outdir dist",
|
||||
start: "bun run dist/index.js",
|
||||
},
|
||||
dependencies: {
|
||||
"@hono/node-server": "^1.12.0",
|
||||
hono: "^4.6.0",
|
||||
},
|
||||
devDependencies: {
|
||||
"@types/node": "^22.7.9",
|
||||
typescript: "^5.6.3",
|
||||
},
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "backend", "package.json"),
|
||||
JSON.stringify(backendPackageJson, null, 2)
|
||||
);
|
||||
|
||||
// Backend src/index.ts
|
||||
const backendIndex = `import { Hono } from 'hono'
|
||||
import { serve } from '@hono/node-server'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.json({ message: 'Hello from backend!' })
|
||||
})
|
||||
|
||||
app.get('/api/health', (c) => {
|
||||
return c.json({ status: 'ok' })
|
||||
})
|
||||
|
||||
const port = 3000
|
||||
console.log(\`Server is running on port \${port}\`)
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
})
|
||||
`;
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "backend", "src", "index.ts"),
|
||||
backendIndex
|
||||
);
|
||||
|
||||
// Backend tsconfig.json
|
||||
const backendTsconfig = {
|
||||
compilerOptions: {
|
||||
target: "ES2022",
|
||||
module: "ESNext",
|
||||
lib: ["ES2022"],
|
||||
moduleResolution: "bundler",
|
||||
types: ["bun-types"],
|
||||
strict: true,
|
||||
esModuleInterop: true,
|
||||
skipLibCheck: true,
|
||||
},
|
||||
include: ["src/**/*"],
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "backend", "tsconfig.json"),
|
||||
JSON.stringify(backendTsconfig, null, 2)
|
||||
);
|
||||
|
||||
// Frontend vite.config.ts
|
||||
const frontendViteConfig = `import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
`;
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "frontend", "vite.config.ts"),
|
||||
frontendViteConfig
|
||||
);
|
||||
|
||||
// Frontend src/main.ts
|
||||
const frontendMain = `import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
`;
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "frontend", "src", "main.ts"),
|
||||
frontendMain
|
||||
);
|
||||
|
||||
// Frontend src/App.vue
|
||||
const frontendApp = `<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const message = ref('Loading...')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/health')
|
||||
message.value = res.data.message || 'Connected to backend!'
|
||||
} catch (error) {
|
||||
message.value = 'Failed to connect to backend'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<h1>{{ message }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "frontend", "src", "App.vue"),
|
||||
frontendApp
|
||||
);
|
||||
|
||||
// Frontend index.html
|
||||
const frontendIndexHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${name}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
writeFileSync(
|
||||
join(projectPath, "frontend", "index.html"),
|
||||
frontendIndexHtml
|
||||
);
|
||||
|
||||
writeFileSync(join(projectPath, "frontend", "src", "style.css"), "");
|
||||
|
||||
// README
|
||||
const readme = `# ${name}
|
||||
|
||||
Fullstack project with Vue 3 frontend and Bun backend.
|
||||
|
||||
## Setup
|
||||
|
||||
\`\`\`bash
|
||||
bun install
|
||||
bun run --cwd frontend install
|
||||
bun run --cwd backend install
|
||||
\`\`\`
|
||||
|
||||
## Development
|
||||
|
||||
\`\`\`bash
|
||||
bun run dev
|
||||
\`\`\`
|
||||
|
||||
This will start both frontend (port 5173) and backend (port 3000) concurrently.
|
||||
`;
|
||||
|
||||
writeFileSync(join(projectPath, "README.md"), readme);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Fullstack project "${name}" created successfully at ${projectPath}\n\nStructure:\n- frontend/ (Vite + Vue3)\n- backend/ (Bun + Hono)\n\nNext steps:\n1. cd ${name}\n2. bun install\n3. bun run --cwd frontend install\n4. bun run --cwd backend install\n5. bun run dev`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating project: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// List templates
|
||||
mcpServer.registerTool(
|
||||
{
|
||||
name: "project_template_list",
|
||||
description: "List available project templates",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Available project templates:
|
||||
|
||||
1. **Vite + Vue3 + TypeScript** (project_template_create)
|
||||
- Basic Vue 3 setup with TypeScript
|
||||
- Optional: Pinia, Vue Router, Tailwind CSS
|
||||
|
||||
2. **Fullstack** (project_template_create_fullstack)
|
||||
- Frontend: Vite + Vue3 + TypeScript
|
||||
- Backend: Bun + Hono
|
||||
- Includes proxy configuration for API calls
|
||||
|
||||
Use the respective tool to create a project.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
45
src/utils/logger.ts
Normal file
45
src/utils/logger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Simple logger utility
|
||||
*/
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private level: LogLevel = LogLevel.INFO;
|
||||
|
||||
setLevel(level: LogLevel): void {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
debug(message: string, ...args: unknown[]): void {
|
||||
if (this.level <= LogLevel.DEBUG) {
|
||||
console.debug(`[DEBUG] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
info(message: string, ...args: unknown[]): void {
|
||||
if (this.level <= LogLevel.INFO) {
|
||||
console.info(`[INFO] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string, ...args: unknown[]): void {
|
||||
if (this.level <= LogLevel.WARN) {
|
||||
console.warn(`[WARN] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, ...args: unknown[]): void {
|
||||
if (this.level <= LogLevel.ERROR) {
|
||||
console.error(`[ERROR] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["bun-types", "node"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user