@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js";
|
|
2
|
+
export async function dirHandler(request) {
|
|
3
|
+
const { path } = request.params.arguments || {};
|
|
4
|
+
const nas_host = getNasHost();
|
|
5
|
+
const nas_sid = getNasSid();
|
|
6
|
+
if (!nas_host || !nas_sid) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text: "Not connected to QNAP NAS. Use qnap-connect first." }],
|
|
9
|
+
isError: true
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (typeof path !== "string" || !path) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: "Missing or invalid path argument." }],
|
|
15
|
+
isError: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const url = `${nas_host}/cgi-bin/filemanager/utilRequest.cgi?func=get_list&path=${path}&sid=${nas_sid}&limit=100&start=0`;
|
|
20
|
+
const response = await fetchWithTimeout(url);
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
24
|
+
isError: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: `Error listing directory: ${error.message}` }],
|
|
30
|
+
isError: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js";
|
|
2
|
+
export async function fileInfoHandler(request) {
|
|
3
|
+
const { path, filename } = request.params.arguments || {};
|
|
4
|
+
const nas_host = getNasHost();
|
|
5
|
+
const nas_sid = getNasSid();
|
|
6
|
+
if (!nas_host || !nas_sid) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text: "Not connected to QNAP NAS. Use qnap-connect first." }],
|
|
9
|
+
isError: true
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (typeof path !== "string" || !path || typeof filename !== "string" || !filename) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: "text", text: "Missing or invalid path or filename argument." }],
|
|
15
|
+
isError: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const url = `${nas_host}/cgi-bin/filemanager/utilRequest.cgi?func=stat&sid=${nas_sid}&path=${path}&file_total=1&file_name=${filename}`;
|
|
20
|
+
const response = await fetchWithTimeout(url);
|
|
21
|
+
const text = await response.text();
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text", text: text }],
|
|
24
|
+
isError: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: `Error getting file info: ${error.message}` }],
|
|
30
|
+
isError: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js";
|
|
2
|
+
export async function reportHandler(request) {
|
|
3
|
+
const nas_host = getNasHost();
|
|
4
|
+
const nas_sid = getNasSid();
|
|
5
|
+
if (!nas_host || !nas_sid) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: "text", text: "Not connected to QNAP NAS. Use qnap-connect first." }],
|
|
8
|
+
isError: true
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
// Fetch system info to get model details
|
|
13
|
+
const dc = Date.now();
|
|
14
|
+
const url = `${nas_host}/cgi-bin/sys/sysRequest.cgi?subfunc=sys_info&sid=${nas_sid}&_dc=${dc}`;
|
|
15
|
+
const response = await fetchWithTimeout(url);
|
|
16
|
+
const text = await response.text();
|
|
17
|
+
const modelMatch = text.match(/<displayModelName><!\[CDATA\[([^\]]*)\]\]><\/displayModelName>/);
|
|
18
|
+
const modelName = modelMatch ? modelMatch[1] : "Unknown QNAP Model";
|
|
19
|
+
// Extract IP from host
|
|
20
|
+
const hostUrl = new URL(nas_host);
|
|
21
|
+
const report = [
|
|
22
|
+
`### QNAP Connection Report`,
|
|
23
|
+
`- **Model Name**: ${modelName}`,
|
|
24
|
+
`- **IP Address**: ${hostUrl.hostname}`,
|
|
25
|
+
`- **Connected User**: ${process.env.QNAP_USER || "admin"}`,
|
|
26
|
+
`- **Access URL**: ${nas_host}`
|
|
27
|
+
].join('\n');
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: report }],
|
|
30
|
+
isError: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `Error generating report: ${error.message}` }],
|
|
36
|
+
isError: true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const tools = [
|
|
2
|
+
{
|
|
3
|
+
name: "qnap-connect",
|
|
4
|
+
description: "Connect to a QNAP NAS and obtain a session ID.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
host: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "QNAP NAS host (e.g., http://10.1.1.241:8080)"
|
|
11
|
+
},
|
|
12
|
+
username: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Username"
|
|
15
|
+
},
|
|
16
|
+
password: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Password"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ["host", "username", "password"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "qnap-report",
|
|
26
|
+
description: "Generate a QNAP system report including CPU, memory, disks and volume status.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {},
|
|
30
|
+
required: []
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "qnap-dir",
|
|
35
|
+
description: "List contents of a directory on the QNAP NAS.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
path: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Directory path (e.g., /Public)"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: ["path"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "qnap-file-info",
|
|
49
|
+
description: "Get detailed information about a file on the QNAP NAS.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
path: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Directory path where the file is located"
|
|
56
|
+
},
|
|
57
|
+
filename: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Name of the file"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
required: ["path", "filename"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
];
|
package/handlers.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { connectHandler, initializeApi } from "./tools/connect.js";
|
|
3
|
+
import { reportHandler } from "./tools/report.js";
|
|
4
|
+
import { dirHandler } from "./tools/dir.js";
|
|
5
|
+
import { fileInfoHandler } from "./tools/file_info.js";
|
|
6
|
+
|
|
7
|
+
const toolHandlers: Record<string, (request: CallToolRequest) => Promise<any>> = {
|
|
8
|
+
"qnap-connect": connectHandler,
|
|
9
|
+
"qnap-report": reportHandler,
|
|
10
|
+
"qnap-dir": dirHandler,
|
|
11
|
+
"qnap-file-info": fileInfoHandler,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const callToolHandler = async (request: CallToolRequest) => {
|
|
15
|
+
const handler = toolHandlers[request.params.name];
|
|
16
|
+
if (handler) {
|
|
17
|
+
return handler(request);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { initializeApi };
|
|
23
|
+
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marcelo-ochoa/server-qnap",
|
|
3
|
+
"mcpName": "io.github.marcelo-ochoa/qnap",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"description": "An MCP server for QNAP NAS API.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server-qnap": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
13
|
+
"prepare": "npm run build",
|
|
14
|
+
"watch": "tsc --watch"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.24.2"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22",
|
|
21
|
+
"shx": "^0.3.4",
|
|
22
|
+
"typescript": "^5.6.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.marcelo-ochoa/qnap",
|
|
4
|
+
"description": "MCP server for QNAP NAS API",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/marcelo-ochoa/servers",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "src/qnap"
|
|
9
|
+
},
|
|
10
|
+
"version": "1.0.1",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@marcelo-ochoa/server-qnap",
|
|
15
|
+
"version": "1.0.1",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/server.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { tools } from "./tools.js";
|
|
9
|
+
import { callToolHandler, initializeApi } from "./handlers.js";
|
|
10
|
+
|
|
11
|
+
const prompts = [
|
|
12
|
+
{ name: "qnap-connect: Connect to QNAP NAS", description: "connect to QNAP NAS using host, username and password" },
|
|
13
|
+
{ name: "qnap-report: System Report", description: "show a comprehensive system report with CPU, memory, and disk status" },
|
|
14
|
+
{ name: "qnap-dir: List Directory", description: "list contents of a directory on the QNAP NAS" },
|
|
15
|
+
{ name: "qnap-file-info: File Information", description: "get detailed information about a specific file" }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const PromptsListRequestSchema = z.object({
|
|
19
|
+
method: z.literal("prompts/list"),
|
|
20
|
+
params: z.object({}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export class QnapServer {
|
|
24
|
+
private server: Server;
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
this.server = new Server(
|
|
28
|
+
{
|
|
29
|
+
name: "qnap-mcp-server",
|
|
30
|
+
version: "1.0.1",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
capabilities: {
|
|
34
|
+
tools: {},
|
|
35
|
+
prompts: {},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this.setupHandlers();
|
|
41
|
+
|
|
42
|
+
this.server.onerror = (error) => console.error("[MCP Error]", error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private setupHandlers() {
|
|
46
|
+
this.server.setRequestHandler(PromptsListRequestSchema, async () => {
|
|
47
|
+
return {
|
|
48
|
+
prompts,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
53
|
+
tools: tools,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
57
|
+
return await callToolHandler(request);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async run() {
|
|
62
|
+
const args = process.argv.slice(2);
|
|
63
|
+
const host = args[0];
|
|
64
|
+
|
|
65
|
+
if (host) {
|
|
66
|
+
try {
|
|
67
|
+
await initializeApi(host);
|
|
68
|
+
console.error(`Automatically connected to QNAP NAS at ${host}`);
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
console.error(`Failed to automatically connect to QNAP NAS: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
console.error("Warning: No QNAP host provided as argument. Use qnap-connect tool before using other functionality.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const transport = new StdioServerTransport();
|
|
77
|
+
await this.server.connect(transport);
|
|
78
|
+
console.error("QNAP MCP server running on stdio");
|
|
79
|
+
|
|
80
|
+
process.stdin.on("close", () => {
|
|
81
|
+
console.error("QNAP MCP Server closed");
|
|
82
|
+
this.server.close();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
package/tools/connect.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
|
|
3
|
+
let nas_host: string | null = null;
|
|
4
|
+
let nas_sid: string | null = null;
|
|
5
|
+
|
|
6
|
+
export function getNasHost(): string | null {
|
|
7
|
+
return nas_host;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getNasSid(): string | null {
|
|
11
|
+
return nas_sid;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setNasConnection(host: string, sid: string): void {
|
|
15
|
+
nas_host = host;
|
|
16
|
+
nas_sid = sid;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearNasConnection(): void {
|
|
20
|
+
nas_host = null;
|
|
21
|
+
nas_sid = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function initializeApi(host: string, username?: string, password?: string) {
|
|
25
|
+
const user = username || process.env.QNAP_USER;
|
|
26
|
+
const pwd = password || process.env.QNAP_PASSWORD;
|
|
27
|
+
|
|
28
|
+
if (!user || !pwd) {
|
|
29
|
+
throw new Error("Credentials not provided and QNAP_USER/QNAP_PASSWORD env variables not set.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Directly perform the connection logic
|
|
33
|
+
const b64_pwd = Buffer.from(pwd).toString('base64');
|
|
34
|
+
const url = `${host}/cgi-bin/authLogin.cgi?user=${user}&pwd=${b64_pwd}`;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetchWithTimeout(url);
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
|
|
40
|
+
// Extract SID using regex
|
|
41
|
+
const sidMatch = text.match(/<authSid><!\[CDATA\[([^\]]*)\]\]><\/authSid>/);
|
|
42
|
+
const sid = sidMatch ? sidMatch[1] : null;
|
|
43
|
+
|
|
44
|
+
if (sid) {
|
|
45
|
+
setNasConnection(host, sid);
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Login failed. Response: ${text}`);
|
|
48
|
+
}
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
throw new Error(`Error connecting to QNAP: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fetchWithTimeout(url: string, options: any = {}, timeout = 60000) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
57
|
+
|
|
58
|
+
const headers = {
|
|
59
|
+
...options.headers,
|
|
60
|
+
'Referer': nas_host ? `${nas_host}/cgi-bin/index.cgi` : '',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (nas_sid) {
|
|
64
|
+
(headers as any)['Cookie'] = `NAS_SID=${nas_sid}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
...options,
|
|
70
|
+
headers,
|
|
71
|
+
signal: controller.signal
|
|
72
|
+
});
|
|
73
|
+
clearTimeout(id);
|
|
74
|
+
return response;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
clearTimeout(id);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function connectHandler(request: CallToolRequest) {
|
|
82
|
+
const { host, username, password } = request.params.arguments || {};
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
typeof host !== "string" || !host ||
|
|
86
|
+
typeof username !== "string" || !username ||
|
|
87
|
+
typeof password !== "string" || !password
|
|
88
|
+
) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: "Missing or invalid host, username, or password argument." }],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const b64_pwd = Buffer.from(password).toString('base64');
|
|
96
|
+
const url = `${host}/cgi-bin/authLogin.cgi?user=${username}&pwd=${b64_pwd}`;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetchWithTimeout(url);
|
|
100
|
+
const text = await response.text();
|
|
101
|
+
|
|
102
|
+
// Extract SID using regex
|
|
103
|
+
const sidMatch = text.match(/<authSid><!\[CDATA\[([^\]]*)\]\]><\/authSid>/);
|
|
104
|
+
const sid = sidMatch ? sidMatch[1] : null;
|
|
105
|
+
|
|
106
|
+
if (sid) {
|
|
107
|
+
setNasConnection(host, sid);
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: `Connected successfully to ${host}. SID: ${sid}` }],
|
|
110
|
+
isError: false,
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: `Login failed. Response: ${text}` }],
|
|
115
|
+
isError: true
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text", text: `Error connecting to QNAP: ${error.message}` }],
|
|
121
|
+
isError: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { fetchWithTimeout };
|
package/tools/dir.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js";
|
|
3
|
+
|
|
4
|
+
export async function dirHandler(request: CallToolRequest) {
|
|
5
|
+
const { path } = request.params.arguments || {};
|
|
6
|
+
const nas_host = getNasHost();
|
|
7
|
+
const nas_sid = getNasSid();
|
|
8
|
+
|
|
9
|
+
if (!nas_host || !nas_sid) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: "Not connected to QNAP NAS. Use qnap-connect first." }],
|
|
12
|
+
isError: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof path !== "string" || !path) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: "Missing or invalid path argument." }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const url = `${nas_host}/cgi-bin/filemanager/utilRequest.cgi?func=get_list&path=${path}&sid=${nas_sid}&limit=100&start=0`;
|
|
25
|
+
const response = await fetchWithTimeout(url);
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
30
|
+
isError: false,
|
|
31
|
+
};
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: `Error listing directory: ${error.message}` }],
|
|
35
|
+
isError: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js";
|
|
3
|
+
|
|
4
|
+
export async function fileInfoHandler(request: CallToolRequest) {
|
|
5
|
+
const { path, filename } = request.params.arguments || {};
|
|
6
|
+
const nas_host = getNasHost();
|
|
7
|
+
const nas_sid = getNasSid();
|
|
8
|
+
|
|
9
|
+
if (!nas_host || !nas_sid) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: "Not connected to QNAP NAS. Use qnap-connect first." }],
|
|
12
|
+
isError: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof path !== "string" || !path || typeof filename !== "string" || !filename) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: "Missing or invalid path or filename argument." }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const url = `${nas_host}/cgi-bin/filemanager/utilRequest.cgi?func=stat&sid=${nas_sid}&path=${path}&file_total=1&file_name=${filename}`;
|
|
25
|
+
const response = await fetchWithTimeout(url);
|
|
26
|
+
const text = await response.text();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: text }],
|
|
30
|
+
isError: false,
|
|
31
|
+
};
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: `Error getting file info: ${error.message}` }],
|
|
35
|
+
isError: true
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
package/tools/report.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js";
|
|
3
|
+
|
|
4
|
+
export async function reportHandler(request: CallToolRequest) {
|
|
5
|
+
const nas_host = getNasHost();
|
|
6
|
+
const nas_sid = getNasSid();
|
|
7
|
+
|
|
8
|
+
if (!nas_host || !nas_sid) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: "text", text: "Not connected to QNAP NAS. Use qnap-connect first." }],
|
|
11
|
+
isError: true
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Fetch system info to get model details
|
|
17
|
+
const dc = Date.now();
|
|
18
|
+
const url = `${nas_host}/cgi-bin/sys/sysRequest.cgi?subfunc=sys_info&sid=${nas_sid}&_dc=${dc}`;
|
|
19
|
+
const response = await fetchWithTimeout(url);
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
|
|
22
|
+
const modelMatch = text.match(/<displayModelName><!\[CDATA\[([^\]]*)\]\]><\/displayModelName>/);
|
|
23
|
+
const modelName = modelMatch ? modelMatch[1] : "Unknown QNAP Model";
|
|
24
|
+
|
|
25
|
+
// Extract IP from host
|
|
26
|
+
const hostUrl = new URL(nas_host);
|
|
27
|
+
|
|
28
|
+
const report = [
|
|
29
|
+
`### QNAP Connection Report`,
|
|
30
|
+
`- **Model Name**: ${modelName}`,
|
|
31
|
+
`- **IP Address**: ${hostUrl.hostname}`,
|
|
32
|
+
`- **Connected User**: ${process.env.QNAP_USER || "admin"}`,
|
|
33
|
+
`- **Access URL**: ${nas_host}`
|
|
34
|
+
].join('\n');
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: report }],
|
|
38
|
+
isError: false,
|
|
39
|
+
};
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `Error generating report: ${error.message}` }],
|
|
43
|
+
isError: true
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
package/tools.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const tools = [
|
|
2
|
+
{
|
|
3
|
+
name: "qnap-connect",
|
|
4
|
+
description: "Connect to a QNAP NAS and obtain a session ID.",
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: "object",
|
|
7
|
+
properties: {
|
|
8
|
+
host: {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "QNAP NAS host (e.g., http://10.1.1.241:8080)"
|
|
11
|
+
},
|
|
12
|
+
username: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Username"
|
|
15
|
+
},
|
|
16
|
+
password: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Password"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ["host", "username", "password"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "qnap-report",
|
|
26
|
+
description: "Generate a QNAP system report including CPU, memory, disks and volume status.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {},
|
|
30
|
+
required: []
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "qnap-dir",
|
|
35
|
+
description: "List contents of a directory on the QNAP NAS.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
path: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Directory path (e.g., /Public)"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: ["path"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "qnap-file-info",
|
|
49
|
+
description: "Get detailed information about a file on the QNAP NAS.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
path: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Directory path where the file is located"
|
|
56
|
+
},
|
|
57
|
+
filename: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Name of the file"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
required: ["path", "filename"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
];
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"lib": [
|
|
9
|
+
"ESNext"
|
|
10
|
+
],
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"*.ts"
|
|
17
|
+
]
|
|
18
|
+
}
|