@marcelo-ochoa/server-qnap 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +6 -0
- package/CHANGELOG.md +29 -0
- package/Demos.md +172 -0
- package/Dockerfile +29 -0
- package/README.md +89 -0
- package/REFACTORING.md +139 -0
- package/dist/handlers.js +18 -0
- package/dist/index.js +7 -0
- package/dist/server.js +69 -0
- package/dist/tools/connect.js +106 -0
- package/dist/tools/dir.js +33 -0
- package/dist/tools/file_info.js +33 -0
- package/dist/tools/report.js +39 -0
- package/dist/tools.js +65 -0
- package/handlers.ts +23 -0
- package/index.ts +8 -0
- package/package.json +24 -0
- package/server.json +21 -0
- package/server.ts +86 -0
- package/tools/connect.ts +126 -0
- package/tools/dir.ts +38 -0
- package/tools/file_info.ts +38 -0
- package/tools/report.ts +46 -0
- package/tools.ts +65 -0
- package/tsconfig.json +18 -0
package/.dockerignore
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.1] - 2026-02-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Major refactoring for improved maintainability**: Restructured codebase following MySQL MCP server patterns
|
|
9
|
+
- Separated tool handlers into individual files in `tools/` directory (`connect.ts`, `report.ts`, `dir.ts`, `file_info.ts`)
|
|
10
|
+
- Simplified `handlers.ts` to act as a clean dispatcher using a handler registry pattern
|
|
11
|
+
- Refactored `tools.ts` to use array-based tool definitions for consistency
|
|
12
|
+
- Updated `server.ts` to use the new handler dispatcher
|
|
13
|
+
- Improved code organization with better separation of concerns
|
|
14
|
+
- Enhanced error handling and validation in individual tool handlers
|
|
15
|
+
- Added connection state management helper functions
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Prompts array support**: Added prompts capability following MikroTik MCP server pattern
|
|
19
|
+
- Implemented `prompts/list` request handler for better tool discoverability
|
|
20
|
+
- Added prompts for all available tools: `qnap-connect`, `qnap-report`, `qnap-dir`, `qnap-file-info`
|
|
21
|
+
- Enhanced server capabilities to include prompts interface
|
|
22
|
+
|
|
23
|
+
## [1.0.0] - 2026-01-27
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Initial implementation of the QNAP MCP server.
|
|
27
|
+
- Tools: `qnap-connect`, `qnap-report`, `qnap-dir`, `qnap-file-info`.
|
|
28
|
+
- Support for QTS Legacy CGI API.
|
|
29
|
+
|
package/Demos.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Demos
|
|
2
|
+
|
|
3
|
+
Some sample usage scenarios are shown below:
|
|
4
|
+
|
|
5
|
+
## Usage with Claude Desktop
|
|
6
|
+
|
|
7
|
+
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
|
|
8
|
+
|
|
9
|
+
### Docker
|
|
10
|
+
|
|
11
|
+
* when running docker on macOS, use `host.docker.internal` if the server is running on the host network (eg localhost)
|
|
12
|
+
* Credentials are passed via environment variables `QNAP_USER` and `QNAP_PASSWORD`
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"qnap": {
|
|
18
|
+
"command": "docker",
|
|
19
|
+
"args": [
|
|
20
|
+
"run",
|
|
21
|
+
"-i",
|
|
22
|
+
"--rm",
|
|
23
|
+
"-e",
|
|
24
|
+
"QNAP_USER=admin",
|
|
25
|
+
"-e",
|
|
26
|
+
"QNAP_PASSWORD=password",
|
|
27
|
+
"mochoa/mcp-qnap",
|
|
28
|
+
"http://10.1.1.241:8080"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### NPX
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"qnap": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": [
|
|
42
|
+
"-y",
|
|
43
|
+
"@marcelo-ochoa/server-qnap",
|
|
44
|
+
"http://10.1.1.241:8080"
|
|
45
|
+
],
|
|
46
|
+
"env": {
|
|
47
|
+
"QNAP_USER": "admin",
|
|
48
|
+
"QNAP_PASSWORD": "password"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Demo Prompts
|
|
56
|
+
|
|
57
|
+
Sample prompts using above server.
|
|
58
|
+
|
|
59
|
+
- qnap-connect http://10.1.1.241:8080 admin password
|
|
60
|
+
- qnap-report
|
|
61
|
+
- qnap-dir /Public
|
|
62
|
+
- qnap-file-info /Public/test.txt
|
|
63
|
+
- analyze backup files in several subdirectories of `/backup/stacks` on the QNAP NAS. For each subdirectory compare the two most recent backups (daily or weekly, identified by filenames like `alldb_backup-2.sql.gz` for weekly and `alldb_backup-09.sql.gz` for daily). The comparison should be based on file size, considering backups with similar sizes as correct. Generate a report indicating the status (OK or Error) for each directory based on this comparison.
|
|
64
|
+
|
|
65
|
+
### Backup Analysis Report Example
|
|
66
|
+
|
|
67
|
+
Busing information about files of sub-folders from qnap-dir /backup/stacks
|
|
68
|
+
- ayudarg
|
|
69
|
+
- glpi
|
|
70
|
+
- invop
|
|
71
|
+
- kanboard
|
|
72
|
+
- mon
|
|
73
|
+
- moodle
|
|
74
|
+
- mygis
|
|
75
|
+
- ocular-vet
|
|
76
|
+
- patogis
|
|
77
|
+
- piexa
|
|
78
|
+
- web
|
|
79
|
+
- yact
|
|
80
|
+
files into above sub-dirs are backups daily or weekly, for example: alldb_backup-2.sql.gz (meaning last backup of week 2 of current month) alldb_backup-09.sql.gz (meaning last daily backup) in some folder there are weekly backups and other are daily backups. please compare last (weekly/daily) with the previous one sorted descending by modification date using the size information of the file. A correct backup will be similar in size, for example 31.41 Mb and 31.33Mb. Make report with directories with status of the backups showing OK or Error based on explained failed.
|
|
81
|
+
|
|
82
|
+
I have verified the two most recent backup files for each directory, comparing their timestamps and file sizes. "Similar size" is interpreted as a difference of less than 5%, which indicates a successful backup chain.
|
|
83
|
+
|
|
84
|
+
#### QNAP Backup Status Report
|
|
85
|
+
**Date:** 2026-02-09
|
|
86
|
+
|
|
87
|
+
| Directory | Frequency | Latest Backup | Previous Backup | Size Comparison | Status |
|
|
88
|
+
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
89
|
+
| **ayudarg** | Weekly | `alldb_backup-2.sql.gz`<br>(Feb 08, 868.7 KB) | `alldb_backup-1.sql.gz`<br>(Feb 01, 868.7 KB) | Diff: 2 bytes (~0%) | **OK** |
|
|
90
|
+
| **glpi** | Daily | `alldb_backup-09.sql.gz`<br>(Feb 09, 32.9 MB) | `alldb_backup-08.sql.gz`<br>(Feb 08, 32.8 MB) | Diff: ~89 KB (~0.2%) | **OK** |
|
|
91
|
+
| **invop** | Weekly | `alldb_backup-2.sql.gz`<br>(Feb 08, 1.41 MB) | `alldb_backup-1.sql.gz`<br>(Feb 01, 1.41 MB) | Diff: 1 byte (~0%) | **OK** |
|
|
92
|
+
| **kanboard** | Daily | `alldb_backup-09.sql.gz`<br>(Feb 09, 2.97 MB) | `alldb_backup-08.sql.gz`<br>(Feb 08, 2.92 MB) | Diff: ~45 KB (~1.5%) | **OK** |
|
|
93
|
+
| **mon** | Weekly | `alldb_backup-2.sql.gz`<br>(Feb 08, 33.6 MB) | `alldb_backup-1.sql.gz`<br>(Feb 01, 33.7 MB) | Diff: ~117 KB (~0.3%) | **OK** |
|
|
94
|
+
| **moodle** | Daily | `full-backup-09.sql.gz`<br>(Feb 09, 654 MB) | `full-backup-08.sql.gz`<br>(Feb 08, 653 MB) | Diff: ~234 KB (~0.03%) | **OK** |
|
|
95
|
+
| **mygis** | Weekly | `full-backup-2.dump`<br>(Feb 08, 314 KB) | `full-backup-1.dump`<br>(Feb 01, 314 KB) | Diff: 0 bytes (0%) | **OK** |
|
|
96
|
+
| **ocular-vet** | Weekly | `backup-OcularVetDB-2.dump`<br>(Feb 08, 251 KB) | `backup-OcularVetDB-1.dump`<br>(Feb 01, 251 KB) | Diff: ~623 bytes (0.2%) | **OK** |
|
|
97
|
+
| **patogis** | Weekly | `full-backup-2.dump`<br>(Feb 08, 438 KB) | `full-backup-1.dump`<br>(Feb 01, 438 KB) | Diff: 0 bytes (0%) | **OK** |
|
|
98
|
+
| **piexa** | Daily | `full-backup-09.sql.gz`<br>(Feb 09, 162 MB) | `full-backup-08.sql.gz`<br>(Feb 08, 163 MB) | Diff: ~2.2 MB (~0.1%) | **OK** |
|
|
99
|
+
| **web** | Daily | `wordpress_backup-09.sql.gz`<br>(Feb 09, 23.8 MB) | `wordpress_backup-08.sql.gz`<br>(Feb 08, 23.8 MB) | Diff: ~12 KB (~0.05%) | **OK** |
|
|
100
|
+
| **yact** | Weekly | `backup-YactDB-2.gz`<br>(Feb 08, 2.14 MB) | `backup-YactDB-1.gz`<br>(Feb 01, 2.14 MB) | Diff: 0 bytes (0%) | **OK** |
|
|
101
|
+
|
|
102
|
+
#### Summary
|
|
103
|
+
All analyzed directories show consistent backup sizes and up-to-date timestamps (Feb 08 for weekly, Feb 09 for daily). No errors were detected.
|
|
104
|
+
|
|
105
|
+
## Using Gemini CLI
|
|
106
|
+
|
|
107
|
+
[Gemini CLI](https://github.com/google-gemini/gemini-cli/)
|
|
108
|
+
is an open-source AI agent that brings the power of Gemini directly
|
|
109
|
+
into your terminal. It provides lightweight access to Gemini, giving you the
|
|
110
|
+
most direct path from your prompt to our model.
|
|
111
|
+
|
|
112
|
+
Using this sample settings.json file at ~/.gemini/ directory:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"mcpServers": {
|
|
117
|
+
"qnap": {
|
|
118
|
+
"command": "npx",
|
|
119
|
+
"args": [
|
|
120
|
+
"-y",
|
|
121
|
+
"@marcelo-ochoa/server-qnap",
|
|
122
|
+
"http://10.1.1.241:8080"
|
|
123
|
+
],
|
|
124
|
+
"env": {
|
|
125
|
+
"QNAP_USER": "admin",
|
|
126
|
+
"QNAP_PASSWORD": "password"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"security": {
|
|
131
|
+
"auth": {
|
|
132
|
+
"selectedType": "gemini-api-key"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"ui": {
|
|
136
|
+
"theme": "ANSI"
|
|
137
|
+
},
|
|
138
|
+
"selectedAuthType": "gemini-api-key",
|
|
139
|
+
"theme": "Dracula"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Sample prompts with Gemini CLI
|
|
144
|
+
|
|
145
|
+
- qnap-connect to http://10.1.1.241:8080 using admin as user and password as password using qnap mcp server
|
|
146
|
+
- qnap-report
|
|
147
|
+
- analyze backup files in several subdirectories of `/backup/stacks` on the QNAP NAS. For each subdirectory compare the two most recent backups (daily or weekly, identified by filenames like `alldb_backup-2.sql.gz` for weekly and `alldb_backup-09.sql.gz` for daily). The comparison should be based on file size, considering backups with similar sizes as correct. Generate a report indicating the status (OK or Error) for each directory based on this comparison.
|
|
148
|
+
|
|
149
|
+
## Using Antigravity Code Editor
|
|
150
|
+
|
|
151
|
+
Put this in `~/.gemini/antigravity/mcp_config.json`
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"mcpServers": {
|
|
156
|
+
"qnap": {
|
|
157
|
+
"command": "docker",
|
|
158
|
+
"args": [
|
|
159
|
+
"run",
|
|
160
|
+
"-i",
|
|
161
|
+
"--rm",
|
|
162
|
+
"-e",
|
|
163
|
+
"QNAP_USER=admin",
|
|
164
|
+
"-e",
|
|
165
|
+
"QNAP_PASSWORD=password",
|
|
166
|
+
"mochoa/mcp-qnap",
|
|
167
|
+
"http://10.1.1.241:8080"
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
FROM node:slim AS builder
|
|
2
|
+
|
|
3
|
+
COPY src/qnap /app
|
|
4
|
+
COPY tsconfig.json /tsconfig.json
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
RUN --mount=type=cache,target=/root/.npm npm install
|
|
9
|
+
|
|
10
|
+
RUN npm run build
|
|
11
|
+
|
|
12
|
+
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
|
|
13
|
+
|
|
14
|
+
FROM node:slim AS release
|
|
15
|
+
|
|
16
|
+
# Update and upgrade to fix OS-level vulnerabilities
|
|
17
|
+
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
|
|
18
|
+
|
|
19
|
+
COPY --from=builder /app/dist /app/dist
|
|
20
|
+
COPY --from=builder /app/package.json /app/package.json
|
|
21
|
+
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
|
22
|
+
|
|
23
|
+
ENV NODE_ENV=production
|
|
24
|
+
|
|
25
|
+
WORKDIR /app
|
|
26
|
+
|
|
27
|
+
RUN npm ci --ignore-scripts --omit-dev
|
|
28
|
+
|
|
29
|
+
ENTRYPOINT ["node", "dist/index.js"]
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# QNAP MCP Server
|
|
2
|
+
|
|
3
|
+
An MCP server implementation for QNAP NAS devices, providing tools to monitor system status, manage files, and generate reports.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
- `qnap-connect`: Connect to a QNAP NAS and obtain a session ID.
|
|
8
|
+
- `host`: The QNAP NAS URL (e.g., `http://10.1.1.241:8080`).
|
|
9
|
+
- `username`: Your admin username.
|
|
10
|
+
- `password`: Your admin password.
|
|
11
|
+
- `qnap-report`: Generate a system report including CPU, memory, disks, and volume status.
|
|
12
|
+
- `qnap-dir`: List the contents of a directory.
|
|
13
|
+
- `path`: The path to list (e.g., `/Public`).
|
|
14
|
+
- `qnap-file-info`: Get detailed information about a specific file.
|
|
15
|
+
- `path`: The directory path.
|
|
16
|
+
- `filename`: The name of the file.
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
### Environment Variables
|
|
21
|
+
|
|
22
|
+
The server can use environment variables and startup arguments for automatic connection:
|
|
23
|
+
|
|
24
|
+
- **`QNAP_USER`**: QNAP admin username.
|
|
25
|
+
- **`QNAP_PASSWORD`**: QNAP admin password.
|
|
26
|
+
|
|
27
|
+
### Startup Arguments
|
|
28
|
+
|
|
29
|
+
1. **`host`**: (Optional) URL of the QNAP NAS (e.g., `http://10.1.1.241:8080`).
|
|
30
|
+
|
|
31
|
+
If the host and environment variables are provided, the server will attempt to connect automatically at startup.
|
|
32
|
+
|
|
33
|
+
### Usage with Claude Desktop
|
|
34
|
+
|
|
35
|
+
Add the following to your `claude_desktop_config.json`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"qnap": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": [
|
|
43
|
+
"-y",
|
|
44
|
+
"@marcelo-ochoa/server-qnap",
|
|
45
|
+
"http://10.1.1.241:8080"
|
|
46
|
+
],
|
|
47
|
+
"env": {
|
|
48
|
+
"QNAP_USER": "admin",
|
|
49
|
+
"QNAP_PASSWORD": "password"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cd src/qnap
|
|
60
|
+
npm install
|
|
61
|
+
npm run build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
## Demos
|
|
66
|
+
|
|
67
|
+
See [Demos](https://github.com/marcelo-ochoa/servers/blob/main/src/qnap/Demos.md) for usage examples with Claude Desktop, Gemini CLI, and Antigravity Code Editor.
|
|
68
|
+
|
|
69
|
+
## Docker
|
|
70
|
+
|
|
71
|
+
Building the container:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
docker build -t mochoa/mcp-qnap -f src/qnap/Dockerfile .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Running the container:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
docker run -i --rm -e QNAP_USER=admin -e QNAP_PASSWORD=password mochoa/mcp-qnap http://10.1.1.241:8080
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Change Log
|
|
84
|
+
|
|
85
|
+
See [Change Log](CHANGELOG.md) for the history of changes.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/REFACTORING.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# QNAP MCP Server Refactoring Summary
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The QNAP MCP server has been refactored to follow the same architectural patterns as the MySQL MCP server, improving code organization, maintainability, and readability.
|
|
5
|
+
|
|
6
|
+
## Changes Made
|
|
7
|
+
|
|
8
|
+
### 1. Tool Handlers Separation (`tools/` directory)
|
|
9
|
+
Created individual handler files for each tool, following the single responsibility principle:
|
|
10
|
+
|
|
11
|
+
- **`tools/connect.ts`**: Handles QNAP NAS connection and authentication
|
|
12
|
+
- Manages connection state (host and session ID)
|
|
13
|
+
- Provides helper functions: `getNasHost()`, `getNasSid()`, `setNasConnection()`, `clearNasConnection()`
|
|
14
|
+
- Exports `fetchWithTimeout()` for use by other handlers
|
|
15
|
+
- Implements `connectHandler()` for the `qnap-connect` tool
|
|
16
|
+
- Implements `initializeApi()` for programmatic connection
|
|
17
|
+
|
|
18
|
+
- **`tools/report.ts`**: Generates QNAP system reports
|
|
19
|
+
- Implements `reportHandler()` for the `qnap-report` tool
|
|
20
|
+
- Fetches system information and formats connection details
|
|
21
|
+
|
|
22
|
+
- **`tools/dir.ts`**: Lists directory contents
|
|
23
|
+
- Implements `dirHandler()` for the `qnap-dir` tool
|
|
24
|
+
- Handles directory listing via QNAP file manager API
|
|
25
|
+
|
|
26
|
+
- **`tools/file_info.ts`**: Retrieves file information
|
|
27
|
+
- Implements `fileInfoHandler()` for the `qnap-file-info` tool
|
|
28
|
+
- Fetches detailed file metadata
|
|
29
|
+
|
|
30
|
+
### 2. Handler Dispatcher Pattern (`handlers.ts`)
|
|
31
|
+
Simplified the handlers file to act as a clean dispatcher:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const toolHandlers: Record<string, (request: CallToolRequest) => Promise<any>> = {
|
|
35
|
+
"qnap-connect": connectHandler,
|
|
36
|
+
"qnap-report": reportHandler,
|
|
37
|
+
"qnap-dir": dirHandler,
|
|
38
|
+
"qnap-file-info": fileInfoHandler,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const callToolHandler = async (request: CallToolRequest) => {
|
|
42
|
+
const handler = toolHandlers[request.params.name];
|
|
43
|
+
if (handler) {
|
|
44
|
+
return handler(request);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Tool Definitions (`tools.ts`)
|
|
51
|
+
Refactored to use a simple array-based structure matching MySQL pattern:
|
|
52
|
+
|
|
53
|
+
**Before:**
|
|
54
|
+
```typescript
|
|
55
|
+
export const QNAP_CONNECT_TOOL: Tool = { ... };
|
|
56
|
+
export const QNAP_REPORT_TOOL: Tool = { ... };
|
|
57
|
+
export const TOOLS = [QNAP_CONNECT_TOOL, QNAP_REPORT_TOOL, ...];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**After:**
|
|
61
|
+
```typescript
|
|
62
|
+
export const tools = [
|
|
63
|
+
{ name: "qnap-connect", description: "...", inputSchema: {...} },
|
|
64
|
+
{ name: "qnap-report", description: "...", inputSchema: {...} },
|
|
65
|
+
...
|
|
66
|
+
];
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Server Updates (`server.ts`)
|
|
70
|
+
Updated to use the new dispatcher pattern:
|
|
71
|
+
|
|
72
|
+
**Before:**
|
|
73
|
+
```typescript
|
|
74
|
+
switch (request.params.name) {
|
|
75
|
+
case "qnap-connect":
|
|
76
|
+
return await handleConnect(request.params.arguments);
|
|
77
|
+
case "qnap-report":
|
|
78
|
+
return await handleReport();
|
|
79
|
+
...
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**After:**
|
|
84
|
+
```typescript
|
|
85
|
+
return await callToolHandler(request);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Benefits
|
|
89
|
+
|
|
90
|
+
1. **Better Code Organization**: Each tool has its own file with focused responsibility
|
|
91
|
+
2. **Improved Maintainability**: Changes to one tool don't affect others
|
|
92
|
+
3. **Consistent Patterns**: Follows the same structure as MySQL MCP server
|
|
93
|
+
4. **Enhanced Readability**: Smaller, focused files are easier to understand
|
|
94
|
+
5. **Better Error Handling**: Each handler validates its own inputs
|
|
95
|
+
6. **Easier Testing**: Individual handlers can be tested in isolation
|
|
96
|
+
7. **Scalability**: Adding new tools is straightforward - just create a new handler file and register it
|
|
97
|
+
|
|
98
|
+
## File Structure Comparison
|
|
99
|
+
|
|
100
|
+
### Before:
|
|
101
|
+
```
|
|
102
|
+
src/qnap/
|
|
103
|
+
├── handlers.ts (183 lines - all logic inline)
|
|
104
|
+
├── tools.ts (58 lines - individual constants)
|
|
105
|
+
├── server.ts (84 lines - switch statement)
|
|
106
|
+
└── tools/
|
|
107
|
+
└── qnap_report_collector.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### After:
|
|
111
|
+
```
|
|
112
|
+
src/qnap/
|
|
113
|
+
├── handlers.ts (24 lines - clean dispatcher)
|
|
114
|
+
├── tools.ts (66 lines - array-based)
|
|
115
|
+
├── server.ts (70 lines - uses dispatcher)
|
|
116
|
+
└── tools/
|
|
117
|
+
├── connect.ts (connection logic + state management)
|
|
118
|
+
├── report.ts (report generation)
|
|
119
|
+
├── dir.ts (directory listing)
|
|
120
|
+
├── file_info.ts (file information)
|
|
121
|
+
└── qnap_report_collector.py
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Migration Notes
|
|
125
|
+
|
|
126
|
+
- All existing functionality is preserved
|
|
127
|
+
- No breaking changes to the API
|
|
128
|
+
- Connection state is now managed through helper functions
|
|
129
|
+
- Each handler validates its own inputs and returns consistent error structures
|
|
130
|
+
- Build process remains unchanged (`npm run build`)
|
|
131
|
+
|
|
132
|
+
## Next Steps
|
|
133
|
+
|
|
134
|
+
Consider these future enhancements:
|
|
135
|
+
1. Add TypeScript interfaces for QNAP API responses
|
|
136
|
+
2. Implement unit tests for individual handlers
|
|
137
|
+
3. Add more comprehensive error messages
|
|
138
|
+
4. Consider adding retry logic for network operations
|
|
139
|
+
5. Add logging/debugging capabilities
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { connectHandler, initializeApi } from "./tools/connect.js";
|
|
2
|
+
import { reportHandler } from "./tools/report.js";
|
|
3
|
+
import { dirHandler } from "./tools/dir.js";
|
|
4
|
+
import { fileInfoHandler } from "./tools/file_info.js";
|
|
5
|
+
const toolHandlers = {
|
|
6
|
+
"qnap-connect": connectHandler,
|
|
7
|
+
"qnap-report": reportHandler,
|
|
8
|
+
"qnap-dir": dirHandler,
|
|
9
|
+
"qnap-file-info": fileInfoHandler,
|
|
10
|
+
};
|
|
11
|
+
export const callToolHandler = async (request) => {
|
|
12
|
+
const handler = toolHandlers[request.params.name];
|
|
13
|
+
if (handler) {
|
|
14
|
+
return handler(request);
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
17
|
+
};
|
|
18
|
+
export { initializeApi };
|
package/dist/index.js
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { tools } from "./tools.js";
|
|
6
|
+
import { callToolHandler, initializeApi } from "./handlers.js";
|
|
7
|
+
const prompts = [
|
|
8
|
+
{ name: "qnap-connect: Connect to QNAP NAS", description: "connect to QNAP NAS using host, username and password" },
|
|
9
|
+
{ name: "qnap-report: System Report", description: "show a comprehensive system report with CPU, memory, and disk status" },
|
|
10
|
+
{ name: "qnap-dir: List Directory", description: "list contents of a directory on the QNAP NAS" },
|
|
11
|
+
{ name: "qnap-file-info: File Information", description: "get detailed information about a specific file" }
|
|
12
|
+
];
|
|
13
|
+
const PromptsListRequestSchema = z.object({
|
|
14
|
+
method: z.literal("prompts/list"),
|
|
15
|
+
params: z.object({}),
|
|
16
|
+
});
|
|
17
|
+
export class QnapServer {
|
|
18
|
+
server;
|
|
19
|
+
constructor() {
|
|
20
|
+
this.server = new Server({
|
|
21
|
+
name: "qnap-mcp-server",
|
|
22
|
+
version: "1.0.1",
|
|
23
|
+
}, {
|
|
24
|
+
capabilities: {
|
|
25
|
+
tools: {},
|
|
26
|
+
prompts: {},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
this.setupHandlers();
|
|
30
|
+
this.server.onerror = (error) => console.error("[MCP Error]", error);
|
|
31
|
+
}
|
|
32
|
+
setupHandlers() {
|
|
33
|
+
this.server.setRequestHandler(PromptsListRequestSchema, async () => {
|
|
34
|
+
return {
|
|
35
|
+
prompts,
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
39
|
+
tools: tools,
|
|
40
|
+
}));
|
|
41
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
|
+
return await callToolHandler(request);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async run() {
|
|
46
|
+
const args = process.argv.slice(2);
|
|
47
|
+
const host = args[0];
|
|
48
|
+
if (host) {
|
|
49
|
+
try {
|
|
50
|
+
await initializeApi(host);
|
|
51
|
+
console.error(`Automatically connected to QNAP NAS at ${host}`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(`Failed to automatically connect to QNAP NAS: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.error("Warning: No QNAP host provided as argument. Use qnap-connect tool before using other functionality.");
|
|
59
|
+
}
|
|
60
|
+
const transport = new StdioServerTransport();
|
|
61
|
+
await this.server.connect(transport);
|
|
62
|
+
console.error("QNAP MCP server running on stdio");
|
|
63
|
+
process.stdin.on("close", () => {
|
|
64
|
+
console.error("QNAP MCP Server closed");
|
|
65
|
+
this.server.close();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
let nas_host = null;
|
|
2
|
+
let nas_sid = null;
|
|
3
|
+
export function getNasHost() {
|
|
4
|
+
return nas_host;
|
|
5
|
+
}
|
|
6
|
+
export function getNasSid() {
|
|
7
|
+
return nas_sid;
|
|
8
|
+
}
|
|
9
|
+
export function setNasConnection(host, sid) {
|
|
10
|
+
nas_host = host;
|
|
11
|
+
nas_sid = sid;
|
|
12
|
+
}
|
|
13
|
+
export function clearNasConnection() {
|
|
14
|
+
nas_host = null;
|
|
15
|
+
nas_sid = null;
|
|
16
|
+
}
|
|
17
|
+
export async function initializeApi(host, username, password) {
|
|
18
|
+
const user = username || process.env.QNAP_USER;
|
|
19
|
+
const pwd = password || process.env.QNAP_PASSWORD;
|
|
20
|
+
if (!user || !pwd) {
|
|
21
|
+
throw new Error("Credentials not provided and QNAP_USER/QNAP_PASSWORD env variables not set.");
|
|
22
|
+
}
|
|
23
|
+
// Directly perform the connection logic
|
|
24
|
+
const b64_pwd = Buffer.from(pwd).toString('base64');
|
|
25
|
+
const url = `${host}/cgi-bin/authLogin.cgi?user=${user}&pwd=${b64_pwd}`;
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetchWithTimeout(url);
|
|
28
|
+
const text = await response.text();
|
|
29
|
+
// Extract SID using regex
|
|
30
|
+
const sidMatch = text.match(/<authSid><!\[CDATA\[([^\]]*)\]\]><\/authSid>/);
|
|
31
|
+
const sid = sidMatch ? sidMatch[1] : null;
|
|
32
|
+
if (sid) {
|
|
33
|
+
setNasConnection(host, sid);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
throw new Error(`Login failed. Response: ${text}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Error connecting to QNAP: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function fetchWithTimeout(url, options = {}, timeout = 60000) {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
46
|
+
const headers = {
|
|
47
|
+
...options.headers,
|
|
48
|
+
'Referer': nas_host ? `${nas_host}/cgi-bin/index.cgi` : '',
|
|
49
|
+
};
|
|
50
|
+
if (nas_sid) {
|
|
51
|
+
headers['Cookie'] = `NAS_SID=${nas_sid}`;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
...options,
|
|
56
|
+
headers,
|
|
57
|
+
signal: controller.signal
|
|
58
|
+
});
|
|
59
|
+
clearTimeout(id);
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
clearTimeout(id);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function connectHandler(request) {
|
|
68
|
+
const { host, username, password } = request.params.arguments || {};
|
|
69
|
+
if (typeof host !== "string" || !host ||
|
|
70
|
+
typeof username !== "string" || !username ||
|
|
71
|
+
typeof password !== "string" || !password) {
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: "Missing or invalid host, username, or password argument." }],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const b64_pwd = Buffer.from(password).toString('base64');
|
|
78
|
+
const url = `${host}/cgi-bin/authLogin.cgi?user=${username}&pwd=${b64_pwd}`;
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetchWithTimeout(url);
|
|
81
|
+
const text = await response.text();
|
|
82
|
+
// Extract SID using regex
|
|
83
|
+
const sidMatch = text.match(/<authSid><!\[CDATA\[([^\]]*)\]\]><\/authSid>/);
|
|
84
|
+
const sid = sidMatch ? sidMatch[1] : null;
|
|
85
|
+
if (sid) {
|
|
86
|
+
setNasConnection(host, sid);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: `Connected successfully to ${host}. SID: ${sid}` }],
|
|
89
|
+
isError: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: `Login failed. Response: ${text}` }],
|
|
95
|
+
isError: true
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: `Error connecting to QNAP: ${error.message}` }],
|
|
102
|
+
isError: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export { fetchWithTimeout };
|