feat: 初次提交

This commit is contained in:
ethan.chen
2026-01-06 17:35:52 +08:00
commit 372b52b214
24 changed files with 4645 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
node_modules/
dist/
.env
.env.local
*.log
.DS_Store
data/
*.db
*.sqlite
*.sqlite3

237
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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}`,
},
],
};
}
);
}

View 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
View 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,
};
}
}
);
}

View 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,
},
],
};
}
);
}

View 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`,
},
],
};
}
}
);
}

View 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,
},
],
};
}
);
}

View 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
View 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
View 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"]
}