@warnyin/nginx-proxy-manager-mcp 0.1.0
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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/api.js +72 -0
- package/dist/config.js +11 -0
- package/dist/index.js +6 -0
- package/dist/server.js +38 -0
- package/dist/tools/access-lists.js +73 -0
- package/dist/tools/audit-log.js +20 -0
- package/dist/tools/certificates.js +109 -0
- package/dist/tools/dead-hosts.js +89 -0
- package/dist/tools/proxy-hosts.js +115 -0
- package/dist/tools/redirection-hosts.js +101 -0
- package/dist/tools/reports.js +11 -0
- package/dist/tools/settings.js +35 -0
- package/dist/tools/streams.js +87 -0
- package/dist/tools/system.js +45 -0
- package/dist/tools/tokens.js +34 -0
- package/dist/tools/users.js +154 -0
- package/dist/util.js +25 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 warnyin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @warnyin/nginx-proxy-manager-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that exposes the **full [Nginx Proxy Manager](https://nginxproxymanager.com) REST API** as tools your AI assistant can call. Drop it into Claude Desktop, Claude Code, Cursor, or any MCP-compatible client and manage proxy hosts, redirections, 404 hosts, streams, SSL certificates, access lists, users, settings, audit log, and reports through natural language.
|
|
4
|
+
|
|
5
|
+
Tool surface is generated from the latest NPM swagger spec — every documented endpoint is reachable, plus an `npm_request` low-level escape hatch for anything obscure.
|
|
6
|
+
|
|
7
|
+
## Install / Run
|
|
8
|
+
|
|
9
|
+
No global install needed. Run via `npx`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y @warnyin/nginx-proxy-manager-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The server talks MCP over **stdio** — point your client at the command above with the env vars described below.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
| Env var | Required | Default | Description |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| `NPM_BASE_URL` | no | `http://127.0.0.1:81/api` | Base URL of NPM API (include `/api`). |
|
|
22
|
+
| `NPM_EMAIL` | one of | – | Login email. Combine with `NPM_PASSWORD` for automatic token issuing + refresh. |
|
|
23
|
+
| `NPM_PASSWORD` | one of | – | Login password. |
|
|
24
|
+
| `NPM_TOKEN` | one of | – | Pre-issued JWT. Skips automatic login (will not auto-refresh once expired). |
|
|
25
|
+
|
|
26
|
+
You must provide either `NPM_TOKEN` *or* the `NPM_EMAIL` + `NPM_PASSWORD` pair.
|
|
27
|
+
|
|
28
|
+
## Claude Desktop / Claude Code config
|
|
29
|
+
|
|
30
|
+
```jsonc
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"nginx-proxy-manager": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["-y", "@warnyin/nginx-proxy-manager-mcp"],
|
|
36
|
+
"env": {
|
|
37
|
+
"NPM_BASE_URL": "http://127.0.0.1:81/api",
|
|
38
|
+
"NPM_EMAIL": "admin@example.com",
|
|
39
|
+
"NPM_PASSWORD": "changeme"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Available tools
|
|
47
|
+
|
|
48
|
+
Grouped by NPM tag — names use the `<group>_<action>` convention.
|
|
49
|
+
|
|
50
|
+
### System
|
|
51
|
+
- `npm_api_root` — `GET /`
|
|
52
|
+
- `npm_version_check` — `GET /version/check`
|
|
53
|
+
- `npm_schema` — `GET /schema`
|
|
54
|
+
- `npm_request` — generic escape hatch for any path / method
|
|
55
|
+
|
|
56
|
+
### Tokens
|
|
57
|
+
- `tokens_login`, `tokens_refresh`, `tokens_validate_2fa`
|
|
58
|
+
|
|
59
|
+
### Users
|
|
60
|
+
- `users_list`, `users_create`, `users_get`, `users_update`, `users_delete`
|
|
61
|
+
- `users_set_auth`, `users_set_permissions`, `users_sudo_login`
|
|
62
|
+
- `users_2fa_get`, `users_2fa_setup`, `users_2fa_enable`, `users_2fa_disable`, `users_2fa_backup_codes`
|
|
63
|
+
|
|
64
|
+
### Proxy hosts
|
|
65
|
+
- `proxy_hosts_list`, `proxy_hosts_create`, `proxy_hosts_get`, `proxy_hosts_update`, `proxy_hosts_delete`
|
|
66
|
+
- `proxy_hosts_enable`, `proxy_hosts_disable`
|
|
67
|
+
|
|
68
|
+
### Redirection hosts
|
|
69
|
+
- `redirection_hosts_list`, `redirection_hosts_create`, `redirection_hosts_get`, `redirection_hosts_update`, `redirection_hosts_delete`
|
|
70
|
+
- `redirection_hosts_enable`, `redirection_hosts_disable`
|
|
71
|
+
|
|
72
|
+
### 404 hosts (dead hosts)
|
|
73
|
+
- `dead_hosts_list`, `dead_hosts_create`, `dead_hosts_get`, `dead_hosts_update`, `dead_hosts_delete`
|
|
74
|
+
- `dead_hosts_enable`, `dead_hosts_disable`
|
|
75
|
+
|
|
76
|
+
### Streams
|
|
77
|
+
- `streams_list`, `streams_create`, `streams_get`, `streams_update`, `streams_delete`
|
|
78
|
+
- `streams_enable`, `streams_disable`
|
|
79
|
+
|
|
80
|
+
### Certificates
|
|
81
|
+
- `certificates_list`, `certificates_create`, `certificates_get`, `certificates_delete`
|
|
82
|
+
- `certificates_download`, `certificates_renew`, `certificates_upload`
|
|
83
|
+
- `certificates_validate`, `certificates_test_http`, `certificates_dns_providers`
|
|
84
|
+
|
|
85
|
+
### Access lists
|
|
86
|
+
- `access_lists_list`, `access_lists_create`, `access_lists_get`, `access_lists_update`, `access_lists_delete`
|
|
87
|
+
|
|
88
|
+
### Audit log
|
|
89
|
+
- `audit_log_list`, `audit_log_get`
|
|
90
|
+
|
|
91
|
+
### Reports
|
|
92
|
+
- `reports_hosts`
|
|
93
|
+
|
|
94
|
+
### Settings
|
|
95
|
+
- `settings_list`, `settings_get`, `settings_update`
|
|
96
|
+
|
|
97
|
+
## Local development
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/warnyin/nginx-proxy-manager-mcp.git
|
|
101
|
+
cd nginx-proxy-manager-mcp
|
|
102
|
+
npm install
|
|
103
|
+
npm run typecheck
|
|
104
|
+
npm run build
|
|
105
|
+
node dist/index.js # or `npm run dev` for tsx watch mode
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class NpmApi {
|
|
2
|
+
config;
|
|
3
|
+
state = null;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
if (config.token) {
|
|
7
|
+
this.state = { token: config.token, expiresAt: Number.MAX_SAFE_INTEGER };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
async ensureToken() {
|
|
11
|
+
if (this.state && this.state.expiresAt - Date.now() > 60_000) {
|
|
12
|
+
return this.state.token;
|
|
13
|
+
}
|
|
14
|
+
if (!this.config.email || !this.config.password) {
|
|
15
|
+
throw new Error("Cached token expired and no NPM_EMAIL/NPM_PASSWORD set for refresh.");
|
|
16
|
+
}
|
|
17
|
+
const url = `${this.config.baseUrl}/tokens`;
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ identity: this.config.email, secret: this.config.password }),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
throw new Error(`Login failed: ${res.status} ${res.statusText} — ${await res.text()}`);
|
|
25
|
+
}
|
|
26
|
+
const data = (await res.json());
|
|
27
|
+
this.state = { token: data.token, expiresAt: new Date(data.expires).getTime() };
|
|
28
|
+
return this.state.token;
|
|
29
|
+
}
|
|
30
|
+
async request(path, opts = {}) {
|
|
31
|
+
const token = await this.ensureToken();
|
|
32
|
+
const url = new URL(`${this.config.baseUrl}${path}`);
|
|
33
|
+
if (opts.query) {
|
|
34
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
35
|
+
if (v === undefined || v === null)
|
|
36
|
+
continue;
|
|
37
|
+
url.searchParams.set(k, String(v));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const headers = {
|
|
41
|
+
Authorization: `Bearer ${token}`,
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
...(opts.headers ?? {}),
|
|
44
|
+
};
|
|
45
|
+
if (opts.body !== undefined && !opts.raw) {
|
|
46
|
+
headers["Content-Type"] = "application/json";
|
|
47
|
+
}
|
|
48
|
+
const res = await fetch(url, {
|
|
49
|
+
method: opts.method ?? "GET",
|
|
50
|
+
headers,
|
|
51
|
+
body: opts.body === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: opts.raw
|
|
54
|
+
? opts.body
|
|
55
|
+
: JSON.stringify(opts.body),
|
|
56
|
+
});
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
let parsed = text;
|
|
59
|
+
if (text && (res.headers.get("content-type") ?? "").includes("application/json")) {
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// leave as text
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
throw new Error(`NPM API ${opts.method ?? "GET"} ${path} → ${res.status} ${res.statusText}: ${typeof parsed === "string" ? parsed : JSON.stringify(parsed)}`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function loadConfig() {
|
|
2
|
+
const baseUrl = (process.env.NPM_BASE_URL ?? "http://127.0.0.1:81/api").replace(/\/+$/, "");
|
|
3
|
+
const email = process.env.NPM_EMAIL;
|
|
4
|
+
const password = process.env.NPM_PASSWORD;
|
|
5
|
+
const token = process.env.NPM_TOKEN;
|
|
6
|
+
if (!token && !(email && password)) {
|
|
7
|
+
throw new Error("Missing credentials: set NPM_TOKEN, or both NPM_EMAIL and NPM_PASSWORD. " +
|
|
8
|
+
"Optionally set NPM_BASE_URL (default: http://127.0.0.1:81/api).");
|
|
9
|
+
}
|
|
10
|
+
return { baseUrl, email, password, token };
|
|
11
|
+
}
|
package/dist/index.js
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { NpmApi } from "./api.js";
|
|
5
|
+
import { registerSystemTools } from "./tools/system.js";
|
|
6
|
+
import { registerTokenTools } from "./tools/tokens.js";
|
|
7
|
+
import { registerUserTools } from "./tools/users.js";
|
|
8
|
+
import { registerProxyHostTools } from "./tools/proxy-hosts.js";
|
|
9
|
+
import { registerRedirectionHostTools } from "./tools/redirection-hosts.js";
|
|
10
|
+
import { registerDeadHostTools } from "./tools/dead-hosts.js";
|
|
11
|
+
import { registerStreamTools } from "./tools/streams.js";
|
|
12
|
+
import { registerCertificateTools } from "./tools/certificates.js";
|
|
13
|
+
import { registerAccessListTools } from "./tools/access-lists.js";
|
|
14
|
+
import { registerAuditLogTools } from "./tools/audit-log.js";
|
|
15
|
+
import { registerReportsTools } from "./tools/reports.js";
|
|
16
|
+
import { registerSettingsTools } from "./tools/settings.js";
|
|
17
|
+
export async function startServer() {
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
const api = new NpmApi(config);
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: "nginx-proxy-manager-mcp",
|
|
22
|
+
version: "0.1.0",
|
|
23
|
+
});
|
|
24
|
+
registerSystemTools(server, api);
|
|
25
|
+
registerTokenTools(server, api);
|
|
26
|
+
registerUserTools(server, api);
|
|
27
|
+
registerProxyHostTools(server, api);
|
|
28
|
+
registerRedirectionHostTools(server, api);
|
|
29
|
+
registerDeadHostTools(server, api);
|
|
30
|
+
registerStreamTools(server, api);
|
|
31
|
+
registerCertificateTools(server, api);
|
|
32
|
+
registerAccessListTools(server, api);
|
|
33
|
+
registerAuditLogTools(server, api);
|
|
34
|
+
registerReportsTools(server, api);
|
|
35
|
+
registerSettingsTools(server, api);
|
|
36
|
+
const transport = new StdioServerTransport();
|
|
37
|
+
await server.connect(transport);
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
const ClientRule = z.object({
|
|
4
|
+
address: z.string(),
|
|
5
|
+
directive: z.enum(["allow", "deny"]),
|
|
6
|
+
});
|
|
7
|
+
const Item = z.object({
|
|
8
|
+
username: z.string(),
|
|
9
|
+
password: z.string(),
|
|
10
|
+
});
|
|
11
|
+
export function registerAccessListTools(server, api) {
|
|
12
|
+
server.tool("access_lists_list", "List all access lists (GET /nginx/access-lists).", { expand: z.string().optional().describe("e.g. `owner,items,clients,proxy_hosts`") }, async ({ expand }) => {
|
|
13
|
+
try {
|
|
14
|
+
return TextResult(await api.request("/nginx/access-lists", { query: { expand } }));
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return ErrorResult(e);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
server.tool("access_lists_create", "Create an access list (POST /nginx/access-lists).", {
|
|
21
|
+
name: z.string(),
|
|
22
|
+
satisfy_any: z.boolean().optional().describe("Match any rule (OR) vs all (AND)"),
|
|
23
|
+
pass_auth: z.boolean().optional(),
|
|
24
|
+
items: z.array(Item).optional().describe("HTTP basic auth credentials"),
|
|
25
|
+
clients: z.array(ClientRule).optional().describe("IP allow/deny rules"),
|
|
26
|
+
extra: z.record(z.unknown()).optional(),
|
|
27
|
+
}, async ({ extra, ...body }) => {
|
|
28
|
+
try {
|
|
29
|
+
return TextResult(await api.request("/nginx/access-lists", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: { ...body, ...(extra ?? {}) },
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
return ErrorResult(e);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
server.tool("access_lists_get", "Get an access list (GET /nginx/access-lists/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
39
|
+
try {
|
|
40
|
+
return TextResult(await api.request(`/nginx/access-lists/${id}`, { query: { expand } }));
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return ErrorResult(e);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
server.tool("access_lists_update", "Update an access list.", {
|
|
47
|
+
id: z.number().int(),
|
|
48
|
+
name: z.string().optional(),
|
|
49
|
+
satisfy_any: z.boolean().optional(),
|
|
50
|
+
pass_auth: z.boolean().optional(),
|
|
51
|
+
items: z.array(Item).optional(),
|
|
52
|
+
clients: z.array(ClientRule).optional(),
|
|
53
|
+
extra: z.record(z.unknown()).optional(),
|
|
54
|
+
}, async ({ id, extra, ...body }) => {
|
|
55
|
+
try {
|
|
56
|
+
return TextResult(await api.request(`/nginx/access-lists/${id}`, {
|
|
57
|
+
method: "PUT",
|
|
58
|
+
body: { ...body, ...(extra ?? {}) },
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return ErrorResult(e);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
server.tool("access_lists_delete", "Delete an access list.", { id: z.number().int() }, async ({ id }) => {
|
|
66
|
+
try {
|
|
67
|
+
return TextResult(await api.request(`/nginx/access-lists/${id}`, { method: "DELETE" }));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return ErrorResult(e);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerAuditLogTools(server, api) {
|
|
4
|
+
server.tool("audit_log_list", "List audit log entries (GET /audit-log).", { expand: z.string().optional().describe("e.g. `user`") }, async ({ expand }) => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/audit-log", { query: { expand } }));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("audit_log_get", "Get a single audit log entry (GET /audit-log/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
13
|
+
try {
|
|
14
|
+
return TextResult(await api.request(`/audit-log/${id}`, { query: { expand } }));
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return ErrorResult(e);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerCertificateTools(server, api) {
|
|
4
|
+
server.tool("certificates_list", "List all certificates (GET /nginx/certificates).", { expand: z.string().optional() }, async ({ expand }) => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/nginx/certificates", { query: { expand } }));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("certificates_create", "Create a certificate (POST /nginx/certificates). For Let's Encrypt set provider='letsencrypt' and supply meta.letsencrypt_agree=true and meta.letsencrypt_email. For DNS challenge include meta.dns_challenge=true with dns_provider/dns_provider_credentials.", {
|
|
13
|
+
provider: z.enum(["letsencrypt", "other"]).default("letsencrypt"),
|
|
14
|
+
nice_name: z.string().optional(),
|
|
15
|
+
domain_names: z.array(z.string()).min(1),
|
|
16
|
+
meta: z
|
|
17
|
+
.record(z.unknown())
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Provider-specific options. LE example: {letsencrypt_email, letsencrypt_agree:true, dns_challenge:false}"),
|
|
20
|
+
extra: z.record(z.unknown()).optional(),
|
|
21
|
+
}, async ({ extra, ...body }) => {
|
|
22
|
+
try {
|
|
23
|
+
return TextResult(await api.request("/nginx/certificates", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
body: { ...body, ...(extra ?? {}) },
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return ErrorResult(e);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("certificates_get", "Get a certificate (GET /nginx/certificates/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
33
|
+
try {
|
|
34
|
+
return TextResult(await api.request(`/nginx/certificates/${id}`, { query: { expand } }));
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return ErrorResult(e);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
server.tool("certificates_delete", "Delete a certificate (DELETE /nginx/certificates/{id}).", { id: z.number().int() }, async ({ id }) => {
|
|
41
|
+
try {
|
|
42
|
+
return TextResult(await api.request(`/nginx/certificates/${id}`, { method: "DELETE" }));
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
return ErrorResult(e);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
server.tool("certificates_download", "Download certificate files (GET /nginx/certificates/{id}/download). Returns the raw zip-like body.", { id: z.number().int() }, async ({ id }) => {
|
|
49
|
+
try {
|
|
50
|
+
return TextResult(await api.request(`/nginx/certificates/${id}/download`));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return ErrorResult(e);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
server.tool("certificates_renew", "Renew a certificate (POST /nginx/certificates/{id}/renew).", { id: z.number().int() }, async ({ id }) => {
|
|
57
|
+
try {
|
|
58
|
+
return TextResult(await api.request(`/nginx/certificates/${id}/renew`, { method: "POST" }));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return ErrorResult(e);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.tool("certificates_upload", "Upload a custom certificate body (POST /nginx/certificates/{id}/upload). Pass PEM contents.", {
|
|
65
|
+
id: z.number().int(),
|
|
66
|
+
certificate: z.string().describe("PEM-encoded certificate"),
|
|
67
|
+
certificate_key: z.string().describe("PEM-encoded private key"),
|
|
68
|
+
intermediate_certificate: z.string().optional(),
|
|
69
|
+
}, async ({ id, ...body }) => {
|
|
70
|
+
try {
|
|
71
|
+
return TextResult(await api.request(`/nginx/certificates/${id}/upload`, { method: "POST", body }));
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return ErrorResult(e);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
server.tool("certificates_validate", "Validate a certificate body (POST /nginx/certificates/validate).", {
|
|
78
|
+
certificate: z.string(),
|
|
79
|
+
certificate_key: z.string(),
|
|
80
|
+
intermediate_certificate: z.string().optional(),
|
|
81
|
+
}, async (body) => {
|
|
82
|
+
try {
|
|
83
|
+
return TextResult(await api.request("/nginx/certificates/validate", { method: "POST", body }));
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return ErrorResult(e);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
server.tool("certificates_test_http", "Verify that a domain answers Let's Encrypt HTTP challenges (POST /nginx/certificates/test-http).", {
|
|
90
|
+
domains: z.array(z.string()).min(1).describe("Domain names to test"),
|
|
91
|
+
}, async ({ domains }) => {
|
|
92
|
+
try {
|
|
93
|
+
return TextResult(await api.request("/nginx/certificates/test-http", {
|
|
94
|
+
query: { domains: domains.join(",") },
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return ErrorResult(e);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
server.tool("certificates_dns_providers", "List supported DNS providers for Let's Encrypt DNS-01 challenges.", {}, async () => {
|
|
102
|
+
try {
|
|
103
|
+
return TextResult(await api.request("/nginx/certificates/dns-providers"));
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return ErrorResult(e);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerDeadHostTools(server, api) {
|
|
4
|
+
server.tool("dead_hosts_list", "List all 404 hosts (GET /nginx/dead-hosts).", { expand: z.string().optional() }, async ({ expand }) => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/nginx/dead-hosts", { query: { expand } }));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("dead_hosts_create", "Create a 404 host (POST /nginx/dead-hosts).", {
|
|
13
|
+
domain_names: z.array(z.string()).min(1),
|
|
14
|
+
certificate_id: z.union([z.number().int(), z.literal("new")]).optional(),
|
|
15
|
+
ssl_forced: z.boolean().optional(),
|
|
16
|
+
hsts_enabled: z.boolean().optional(),
|
|
17
|
+
hsts_subdomains: z.boolean().optional(),
|
|
18
|
+
http2_support: z.boolean().optional(),
|
|
19
|
+
advanced_config: z.string().optional(),
|
|
20
|
+
enabled: z.boolean().optional(),
|
|
21
|
+
meta: z.record(z.unknown()).optional(),
|
|
22
|
+
extra: z.record(z.unknown()).optional(),
|
|
23
|
+
}, async ({ extra, ...body }) => {
|
|
24
|
+
try {
|
|
25
|
+
return TextResult(await api.request("/nginx/dead-hosts", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
body: { ...body, ...(extra ?? {}) },
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return ErrorResult(e);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
server.tool("dead_hosts_get", "Get a 404 host (GET /nginx/dead-hosts/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
35
|
+
try {
|
|
36
|
+
return TextResult(await api.request(`/nginx/dead-hosts/${id}`, { query: { expand } }));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return ErrorResult(e);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.tool("dead_hosts_update", "Update a 404 host.", {
|
|
43
|
+
id: z.number().int(),
|
|
44
|
+
domain_names: z.array(z.string()).optional(),
|
|
45
|
+
certificate_id: z.union([z.number().int(), z.literal("new"), z.literal(0)]).optional(),
|
|
46
|
+
ssl_forced: z.boolean().optional(),
|
|
47
|
+
hsts_enabled: z.boolean().optional(),
|
|
48
|
+
hsts_subdomains: z.boolean().optional(),
|
|
49
|
+
http2_support: z.boolean().optional(),
|
|
50
|
+
advanced_config: z.string().optional(),
|
|
51
|
+
enabled: z.boolean().optional(),
|
|
52
|
+
meta: z.record(z.unknown()).optional(),
|
|
53
|
+
extra: z.record(z.unknown()).optional(),
|
|
54
|
+
}, async ({ id, extra, ...body }) => {
|
|
55
|
+
try {
|
|
56
|
+
return TextResult(await api.request(`/nginx/dead-hosts/${id}`, {
|
|
57
|
+
method: "PUT",
|
|
58
|
+
body: { ...body, ...(extra ?? {}) },
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return ErrorResult(e);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
server.tool("dead_hosts_delete", "Delete a 404 host.", { id: z.number().int() }, async ({ id }) => {
|
|
66
|
+
try {
|
|
67
|
+
return TextResult(await api.request(`/nginx/dead-hosts/${id}`, { method: "DELETE" }));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return ErrorResult(e);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
server.tool("dead_hosts_enable", "Enable a 404 host.", { id: z.number().int() }, async ({ id }) => {
|
|
74
|
+
try {
|
|
75
|
+
return TextResult(await api.request(`/nginx/dead-hosts/${id}/enable`, { method: "POST" }));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return ErrorResult(e);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
server.tool("dead_hosts_disable", "Disable a 404 host.", { id: z.number().int() }, async ({ id }) => {
|
|
82
|
+
try {
|
|
83
|
+
return TextResult(await api.request(`/nginx/dead-hosts/${id}/disable`, { method: "POST" }));
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return ErrorResult(e);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
const Location = z.object({
|
|
4
|
+
path: z.string(),
|
|
5
|
+
forward_scheme: z.enum(["http", "https"]),
|
|
6
|
+
forward_host: z.string(),
|
|
7
|
+
forward_port: z.number().int(),
|
|
8
|
+
advanced_config: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
export function registerProxyHostTools(server, api) {
|
|
11
|
+
server.tool("proxy_hosts_list", "List all proxy hosts (GET /nginx/proxy-hosts).", { expand: z.string().optional().describe("e.g. `owner,access_list,certificate`") }, async ({ expand }) => {
|
|
12
|
+
try {
|
|
13
|
+
return TextResult(await api.request("/nginx/proxy-hosts", { query: { expand } }));
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
return ErrorResult(e);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
server.tool("proxy_hosts_create", "Create a proxy host (POST /nginx/proxy-hosts).", {
|
|
20
|
+
domain_names: z.array(z.string()).min(1),
|
|
21
|
+
forward_scheme: z.enum(["http", "https"]),
|
|
22
|
+
forward_host: z.string(),
|
|
23
|
+
forward_port: z.number().int(),
|
|
24
|
+
certificate_id: z
|
|
25
|
+
.union([z.number().int(), z.literal("new")])
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Existing cert ID, or "new" to request Let\'s Encrypt'),
|
|
28
|
+
access_list_id: z.number().int().optional(),
|
|
29
|
+
ssl_forced: z.boolean().optional(),
|
|
30
|
+
hsts_enabled: z.boolean().optional(),
|
|
31
|
+
hsts_subdomains: z.boolean().optional(),
|
|
32
|
+
http2_support: z.boolean().optional(),
|
|
33
|
+
block_exploits: z.boolean().optional(),
|
|
34
|
+
caching_enabled: z.boolean().optional(),
|
|
35
|
+
allow_websocket_upgrade: z.boolean().optional(),
|
|
36
|
+
advanced_config: z.string().optional(),
|
|
37
|
+
enabled: z.boolean().optional(),
|
|
38
|
+
locations: z.array(Location).optional(),
|
|
39
|
+
meta: z.record(z.unknown()).optional(),
|
|
40
|
+
extra: z.record(z.unknown()).optional(),
|
|
41
|
+
}, async ({ extra, ...body }) => {
|
|
42
|
+
try {
|
|
43
|
+
return TextResult(await api.request("/nginx/proxy-hosts", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: { ...body, ...(extra ?? {}) },
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return ErrorResult(e);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
server.tool("proxy_hosts_get", "Get a proxy host (GET /nginx/proxy-hosts/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
53
|
+
try {
|
|
54
|
+
return TextResult(await api.request(`/nginx/proxy-hosts/${id}`, { query: { expand } }));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return ErrorResult(e);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.tool("proxy_hosts_update", "Update a proxy host (PUT /nginx/proxy-hosts/{id}). Any field omitted is left unchanged.", {
|
|
61
|
+
id: z.number().int(),
|
|
62
|
+
domain_names: z.array(z.string()).optional(),
|
|
63
|
+
forward_scheme: z.enum(["http", "https"]).optional(),
|
|
64
|
+
forward_host: z.string().optional(),
|
|
65
|
+
forward_port: z.number().int().optional(),
|
|
66
|
+
certificate_id: z.union([z.number().int(), z.literal("new"), z.literal(0)]).optional(),
|
|
67
|
+
access_list_id: z.number().int().optional(),
|
|
68
|
+
ssl_forced: z.boolean().optional(),
|
|
69
|
+
hsts_enabled: z.boolean().optional(),
|
|
70
|
+
hsts_subdomains: z.boolean().optional(),
|
|
71
|
+
http2_support: z.boolean().optional(),
|
|
72
|
+
block_exploits: z.boolean().optional(),
|
|
73
|
+
caching_enabled: z.boolean().optional(),
|
|
74
|
+
allow_websocket_upgrade: z.boolean().optional(),
|
|
75
|
+
advanced_config: z.string().optional(),
|
|
76
|
+
enabled: z.boolean().optional(),
|
|
77
|
+
locations: z.array(Location).optional(),
|
|
78
|
+
meta: z.record(z.unknown()).optional(),
|
|
79
|
+
extra: z.record(z.unknown()).optional(),
|
|
80
|
+
}, async ({ id, extra, ...body }) => {
|
|
81
|
+
try {
|
|
82
|
+
return TextResult(await api.request(`/nginx/proxy-hosts/${id}`, {
|
|
83
|
+
method: "PUT",
|
|
84
|
+
body: { ...body, ...(extra ?? {}) },
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
return ErrorResult(e);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
server.tool("proxy_hosts_delete", "Delete a proxy host (DELETE /nginx/proxy-hosts/{id}).", { id: z.number().int() }, async ({ id }) => {
|
|
92
|
+
try {
|
|
93
|
+
return TextResult(await api.request(`/nginx/proxy-hosts/${id}`, { method: "DELETE" }));
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return ErrorResult(e);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.tool("proxy_hosts_enable", "Enable a proxy host (POST /nginx/proxy-hosts/{id}/enable).", { id: z.number().int() }, async ({ id }) => {
|
|
100
|
+
try {
|
|
101
|
+
return TextResult(await api.request(`/nginx/proxy-hosts/${id}/enable`, { method: "POST" }));
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
return ErrorResult(e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
server.tool("proxy_hosts_disable", "Disable a proxy host (POST /nginx/proxy-hosts/{id}/disable).", { id: z.number().int() }, async ({ id }) => {
|
|
108
|
+
try {
|
|
109
|
+
return TextResult(await api.request(`/nginx/proxy-hosts/${id}/disable`, { method: "POST" }));
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return ErrorResult(e);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerRedirectionHostTools(server, api) {
|
|
4
|
+
server.tool("redirection_hosts_list", "List all redirection hosts (GET /nginx/redirection-hosts).", { expand: z.string().optional() }, async ({ expand }) => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/nginx/redirection-hosts", { query: { expand } }));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("redirection_hosts_create", "Create a redirection host (POST /nginx/redirection-hosts).", {
|
|
13
|
+
domain_names: z.array(z.string()).min(1),
|
|
14
|
+
forward_scheme: z.enum(["auto", "http", "https"]).default("auto"),
|
|
15
|
+
forward_domain_name: z.string().describe("Target domain (no scheme)"),
|
|
16
|
+
forward_http_code: z.union([z.literal(300), z.literal(301), z.literal(302), z.literal(307), z.literal(308)]).default(301),
|
|
17
|
+
preserve_path: z.boolean().optional(),
|
|
18
|
+
certificate_id: z.union([z.number().int(), z.literal("new")]).optional(),
|
|
19
|
+
ssl_forced: z.boolean().optional(),
|
|
20
|
+
hsts_enabled: z.boolean().optional(),
|
|
21
|
+
hsts_subdomains: z.boolean().optional(),
|
|
22
|
+
http2_support: z.boolean().optional(),
|
|
23
|
+
block_exploits: z.boolean().optional(),
|
|
24
|
+
advanced_config: z.string().optional(),
|
|
25
|
+
enabled: z.boolean().optional(),
|
|
26
|
+
meta: z.record(z.unknown()).optional(),
|
|
27
|
+
extra: z.record(z.unknown()).optional(),
|
|
28
|
+
}, async ({ extra, ...body }) => {
|
|
29
|
+
try {
|
|
30
|
+
return TextResult(await api.request("/nginx/redirection-hosts", {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: { ...body, ...(extra ?? {}) },
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return ErrorResult(e);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
server.tool("redirection_hosts_get", "Get a redirection host (GET /nginx/redirection-hosts/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
40
|
+
try {
|
|
41
|
+
return TextResult(await api.request(`/nginx/redirection-hosts/${id}`, { query: { expand } }));
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
return ErrorResult(e);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
server.tool("redirection_hosts_update", "Update a redirection host (PUT /nginx/redirection-hosts/{id}).", {
|
|
48
|
+
id: z.number().int(),
|
|
49
|
+
domain_names: z.array(z.string()).optional(),
|
|
50
|
+
forward_scheme: z.enum(["auto", "http", "https"]).optional(),
|
|
51
|
+
forward_domain_name: z.string().optional(),
|
|
52
|
+
forward_http_code: z
|
|
53
|
+
.union([z.literal(300), z.literal(301), z.literal(302), z.literal(307), z.literal(308)])
|
|
54
|
+
.optional(),
|
|
55
|
+
preserve_path: z.boolean().optional(),
|
|
56
|
+
certificate_id: z.union([z.number().int(), z.literal("new"), z.literal(0)]).optional(),
|
|
57
|
+
ssl_forced: z.boolean().optional(),
|
|
58
|
+
hsts_enabled: z.boolean().optional(),
|
|
59
|
+
hsts_subdomains: z.boolean().optional(),
|
|
60
|
+
http2_support: z.boolean().optional(),
|
|
61
|
+
block_exploits: z.boolean().optional(),
|
|
62
|
+
advanced_config: z.string().optional(),
|
|
63
|
+
enabled: z.boolean().optional(),
|
|
64
|
+
meta: z.record(z.unknown()).optional(),
|
|
65
|
+
extra: z.record(z.unknown()).optional(),
|
|
66
|
+
}, async ({ id, extra, ...body }) => {
|
|
67
|
+
try {
|
|
68
|
+
return TextResult(await api.request(`/nginx/redirection-hosts/${id}`, {
|
|
69
|
+
method: "PUT",
|
|
70
|
+
body: { ...body, ...(extra ?? {}) },
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return ErrorResult(e);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
server.tool("redirection_hosts_delete", "Delete a redirection host (DELETE /nginx/redirection-hosts/{id}).", { id: z.number().int() }, async ({ id }) => {
|
|
78
|
+
try {
|
|
79
|
+
return TextResult(await api.request(`/nginx/redirection-hosts/${id}`, { method: "DELETE" }));
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return ErrorResult(e);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
server.tool("redirection_hosts_enable", "Enable a redirection host.", { id: z.number().int() }, async ({ id }) => {
|
|
86
|
+
try {
|
|
87
|
+
return TextResult(await api.request(`/nginx/redirection-hosts/${id}/enable`, { method: "POST" }));
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
return ErrorResult(e);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.tool("redirection_hosts_disable", "Disable a redirection host.", { id: z.number().int() }, async ({ id }) => {
|
|
94
|
+
try {
|
|
95
|
+
return TextResult(await api.request(`/nginx/redirection-hosts/${id}/disable`, { method: "POST" }));
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
return ErrorResult(e);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
2
|
+
export function registerReportsTools(server, api) {
|
|
3
|
+
server.tool("reports_hosts", "Get traffic counts grouped by host type (GET /reports/hosts).", {}, async () => {
|
|
4
|
+
try {
|
|
5
|
+
return TextResult(await api.request("/reports/hosts"));
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
return ErrorResult(e);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerSettingsTools(server, api) {
|
|
4
|
+
server.tool("settings_list", "List all settings (GET /settings).", {}, async () => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/settings"));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("settings_get", "Get a specific setting by id (GET /settings/{id}).", { id: z.string().describe("Setting key, e.g. `default-site`") }, async ({ id }) => {
|
|
13
|
+
try {
|
|
14
|
+
return TextResult(await api.request(`/settings/${encodeURIComponent(id)}`));
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return ErrorResult(e);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
server.tool("settings_update", "Update a setting (PUT /settings/{id}).", {
|
|
21
|
+
id: z.string(),
|
|
22
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.record(z.unknown())]),
|
|
23
|
+
meta: z.record(z.unknown()).optional(),
|
|
24
|
+
}, async ({ id, ...body }) => {
|
|
25
|
+
try {
|
|
26
|
+
return TextResult(await api.request(`/settings/${encodeURIComponent(id)}`, {
|
|
27
|
+
method: "PUT",
|
|
28
|
+
body,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return ErrorResult(e);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerStreamTools(server, api) {
|
|
4
|
+
server.tool("streams_list", "List all streams (GET /nginx/streams).", { expand: z.string().optional() }, async ({ expand }) => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/nginx/streams", { query: { expand } }));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("streams_create", "Create a stream (POST /nginx/streams). At least one of tcp_forwarding/udp_forwarding must be true.", {
|
|
13
|
+
incoming_port: z.number().int(),
|
|
14
|
+
forwarding_host: z.string(),
|
|
15
|
+
forwarding_port: z.number().int(),
|
|
16
|
+
tcp_forwarding: z.boolean().optional(),
|
|
17
|
+
udp_forwarding: z.boolean().optional(),
|
|
18
|
+
certificate_id: z.union([z.number().int(), z.literal("new")]).optional(),
|
|
19
|
+
enabled: z.boolean().optional(),
|
|
20
|
+
meta: z.record(z.unknown()).optional(),
|
|
21
|
+
extra: z.record(z.unknown()).optional(),
|
|
22
|
+
}, async ({ extra, ...body }) => {
|
|
23
|
+
try {
|
|
24
|
+
return TextResult(await api.request("/nginx/streams", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: { ...body, ...(extra ?? {}) },
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return ErrorResult(e);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
server.tool("streams_get", "Get a stream (GET /nginx/streams/{id}).", { id: z.number().int(), expand: z.string().optional() }, async ({ id, expand }) => {
|
|
34
|
+
try {
|
|
35
|
+
return TextResult(await api.request(`/nginx/streams/${id}`, { query: { expand } }));
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
return ErrorResult(e);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
server.tool("streams_update", "Update a stream.", {
|
|
42
|
+
id: z.number().int(),
|
|
43
|
+
incoming_port: z.number().int().optional(),
|
|
44
|
+
forwarding_host: z.string().optional(),
|
|
45
|
+
forwarding_port: z.number().int().optional(),
|
|
46
|
+
tcp_forwarding: z.boolean().optional(),
|
|
47
|
+
udp_forwarding: z.boolean().optional(),
|
|
48
|
+
certificate_id: z.union([z.number().int(), z.literal("new"), z.literal(0)]).optional(),
|
|
49
|
+
enabled: z.boolean().optional(),
|
|
50
|
+
meta: z.record(z.unknown()).optional(),
|
|
51
|
+
extra: z.record(z.unknown()).optional(),
|
|
52
|
+
}, async ({ id, extra, ...body }) => {
|
|
53
|
+
try {
|
|
54
|
+
return TextResult(await api.request(`/nginx/streams/${id}`, {
|
|
55
|
+
method: "PUT",
|
|
56
|
+
body: { ...body, ...(extra ?? {}) },
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return ErrorResult(e);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
server.tool("streams_delete", "Delete a stream.", { id: z.number().int() }, async ({ id }) => {
|
|
64
|
+
try {
|
|
65
|
+
return TextResult(await api.request(`/nginx/streams/${id}`, { method: "DELETE" }));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return ErrorResult(e);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
server.tool("streams_enable", "Enable a stream.", { id: z.number().int() }, async ({ id }) => {
|
|
72
|
+
try {
|
|
73
|
+
return TextResult(await api.request(`/nginx/streams/${id}/enable`, { method: "POST" }));
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
return ErrorResult(e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
server.tool("streams_disable", "Disable a stream.", { id: z.number().int() }, async ({ id }) => {
|
|
80
|
+
try {
|
|
81
|
+
return TextResult(await api.request(`/nginx/streams/${id}/disable`, { method: "POST" }));
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return ErrorResult(e);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerSystemTools(server, api) {
|
|
4
|
+
server.tool("npm_api_root", "Get the API root info (status + version).", {}, async () => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/"));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("npm_version_check", "Check whether a newer Nginx Proxy Manager release is available.", {}, async () => {
|
|
13
|
+
try {
|
|
14
|
+
return TextResult(await api.request("/version/check"));
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
return ErrorResult(e);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
server.tool("npm_schema", "Retrieve the full NPM API schema (OpenAPI/Swagger).", {}, async () => {
|
|
21
|
+
try {
|
|
22
|
+
return TextResult(await api.request("/schema"));
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return ErrorResult(e);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
server.tool("npm_request", "Low-level escape hatch. Make any authenticated request to the NPM API. Use when no typed tool matches. The path must start with `/` and be relative to the API base (e.g. `/nginx/proxy-hosts`).", {
|
|
29
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE"]).default("GET"),
|
|
30
|
+
path: z.string().describe("Path relative to API base, starting with `/`"),
|
|
31
|
+
query: z.record(z.string()).optional().describe("Querystring params"),
|
|
32
|
+
body: z.unknown().optional().describe("JSON request body"),
|
|
33
|
+
}, async ({ method, path, query, body }) => {
|
|
34
|
+
try {
|
|
35
|
+
return TextResult(await api.request(path, {
|
|
36
|
+
method: method,
|
|
37
|
+
query,
|
|
38
|
+
body,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
return ErrorResult(e);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerTokenTools(server, api) {
|
|
4
|
+
server.tool("tokens_login", "Generate a new JWT token by signing in with email/password (POST /tokens).", {
|
|
5
|
+
identity: z.string().describe("User email"),
|
|
6
|
+
secret: z.string().describe("User password"),
|
|
7
|
+
}, async ({ identity, secret }) => {
|
|
8
|
+
try {
|
|
9
|
+
return TextResult(await api.request("/tokens", { method: "POST", body: { identity, secret } }));
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
return ErrorResult(e);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
server.tool("tokens_refresh", "Refresh the current JWT token (GET /tokens).", {}, async () => {
|
|
16
|
+
try {
|
|
17
|
+
return TextResult(await api.request("/tokens"));
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return ErrorResult(e);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
server.tool("tokens_validate_2fa", "Submit a 2FA code to obtain a fully authenticated token (POST /tokens/2fa).", {
|
|
24
|
+
token: z.string().describe("Partially authenticated token returned from login"),
|
|
25
|
+
code: z.string().describe("6-digit TOTP or backup code"),
|
|
26
|
+
}, async ({ token, code }) => {
|
|
27
|
+
try {
|
|
28
|
+
return TextResult(await api.request("/tokens/2fa", { method: "POST", body: { token, code } }));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return ErrorResult(e);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TextResult, ErrorResult } from "../util.js";
|
|
3
|
+
export function registerUserTools(server, api) {
|
|
4
|
+
server.tool("users_list", "List all users (GET /users).", { expand: z.string().optional().describe("e.g. `permissions`") }, async ({ expand }) => {
|
|
5
|
+
try {
|
|
6
|
+
return TextResult(await api.request("/users", { query: { expand } }));
|
|
7
|
+
}
|
|
8
|
+
catch (e) {
|
|
9
|
+
return ErrorResult(e);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
server.tool("users_create", "Create a new user (POST /users).", {
|
|
13
|
+
name: z.string(),
|
|
14
|
+
nickname: z.string().optional(),
|
|
15
|
+
email: z.string().email(),
|
|
16
|
+
roles: z.array(z.string()).optional().describe('e.g. ["admin"]'),
|
|
17
|
+
is_disabled: z.boolean().optional(),
|
|
18
|
+
auth: z
|
|
19
|
+
.object({ type: z.string().default("password"), secret: z.string() })
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('Inline auth, e.g. {"type":"password","secret":"..."}'),
|
|
22
|
+
extra: z.record(z.unknown()).optional(),
|
|
23
|
+
}, async ({ extra, ...rest }) => {
|
|
24
|
+
try {
|
|
25
|
+
return TextResult(await api.request("/users", { method: "POST", body: { ...rest, ...(extra ?? {}) } }));
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return ErrorResult(e);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
server.tool("users_get", "Get a user by ID (GET /users/{id}). Use id `me` for the current user.", {
|
|
32
|
+
id: z.union([z.number().int(), z.literal("me")]),
|
|
33
|
+
expand: z.string().optional(),
|
|
34
|
+
}, async ({ id, expand }) => {
|
|
35
|
+
try {
|
|
36
|
+
return TextResult(await api.request(`/users/${id}`, { query: { expand } }));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return ErrorResult(e);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.tool("users_update", "Update a user (PUT /users/{id}).", {
|
|
43
|
+
id: z.union([z.number().int(), z.literal("me")]),
|
|
44
|
+
name: z.string().optional(),
|
|
45
|
+
nickname: z.string().optional(),
|
|
46
|
+
email: z.string().email().optional(),
|
|
47
|
+
roles: z.array(z.string()).optional(),
|
|
48
|
+
is_disabled: z.boolean().optional(),
|
|
49
|
+
extra: z.record(z.unknown()).optional(),
|
|
50
|
+
}, async ({ id, extra, ...rest }) => {
|
|
51
|
+
try {
|
|
52
|
+
return TextResult(await api.request(`/users/${id}`, {
|
|
53
|
+
method: "PUT",
|
|
54
|
+
body: { ...rest, ...(extra ?? {}) },
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return ErrorResult(e);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
server.tool("users_delete", "Delete a user (DELETE /users/{id}).", { id: z.number().int() }, async ({ id }) => {
|
|
62
|
+
try {
|
|
63
|
+
return TextResult(await api.request(`/users/${id}`, { method: "DELETE" }));
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return ErrorResult(e);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
server.tool("users_set_auth", "Update user authentication (e.g. password) (PUT /users/{id}/auth).", {
|
|
70
|
+
id: z.union([z.number().int(), z.literal("me")]),
|
|
71
|
+
type: z.string().default("password"),
|
|
72
|
+
current: z.string().optional().describe("Current password (when changing own password)"),
|
|
73
|
+
secret: z.string().describe("New password"),
|
|
74
|
+
}, async ({ id, ...body }) => {
|
|
75
|
+
try {
|
|
76
|
+
return TextResult(await api.request(`/users/${id}/auth`, { method: "PUT", body }));
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return ErrorResult(e);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
server.tool("users_set_permissions", "Update user permissions (PUT /users/{id}/permissions).", {
|
|
83
|
+
id: z.number().int(),
|
|
84
|
+
visibility: z.enum(["all", "user"]).optional(),
|
|
85
|
+
proxy_hosts: z.enum(["hidden", "view", "manage"]).optional(),
|
|
86
|
+
redirection_hosts: z.enum(["hidden", "view", "manage"]).optional(),
|
|
87
|
+
dead_hosts: z.enum(["hidden", "view", "manage"]).optional(),
|
|
88
|
+
streams: z.enum(["hidden", "view", "manage"]).optional(),
|
|
89
|
+
access_lists: z.enum(["hidden", "view", "manage"]).optional(),
|
|
90
|
+
certificates: z.enum(["hidden", "view", "manage"]).optional(),
|
|
91
|
+
extra: z.record(z.unknown()).optional(),
|
|
92
|
+
}, async ({ id, extra, ...rest }) => {
|
|
93
|
+
try {
|
|
94
|
+
return TextResult(await api.request(`/users/${id}/permissions`, {
|
|
95
|
+
method: "PUT",
|
|
96
|
+
body: { ...rest, ...(extra ?? {}) },
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return ErrorResult(e);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
server.tool("users_sudo_login", "Login as another user (POST /users/{id}/login). Returns a token impersonating that user.", { id: z.number().int() }, async ({ id }) => {
|
|
104
|
+
try {
|
|
105
|
+
return TextResult(await api.request(`/users/${id}/login`, { method: "POST" }));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return ErrorResult(e);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.tool("users_2fa_get", "Get 2FA status for a user (GET /users/{id}/2fa).", { id: z.union([z.number().int(), z.literal("me")]) }, async ({ id }) => {
|
|
112
|
+
try {
|
|
113
|
+
return TextResult(await api.request(`/users/${id}/2fa`));
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
return ErrorResult(e);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
server.tool("users_2fa_setup", "Start 2FA setup for a user (POST /users/{id}/2fa). Returns QR/secret.", { id: z.union([z.number().int(), z.literal("me")]) }, async ({ id }) => {
|
|
120
|
+
try {
|
|
121
|
+
return TextResult(await api.request(`/users/${id}/2fa`, { method: "POST" }));
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return ErrorResult(e);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.tool("users_2fa_enable", "Verify and enable 2FA (POST /users/{id}/2fa/enable).", {
|
|
128
|
+
id: z.union([z.number().int(), z.literal("me")]),
|
|
129
|
+
code: z.string().describe("6-digit TOTP code"),
|
|
130
|
+
}, async ({ id, code }) => {
|
|
131
|
+
try {
|
|
132
|
+
return TextResult(await api.request(`/users/${id}/2fa/enable`, { method: "POST", body: { code } }));
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return ErrorResult(e);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
server.tool("users_2fa_disable", "Disable 2FA for a user (DELETE /users/{id}/2fa).", { id: z.union([z.number().int(), z.literal("me")]) }, async ({ id }) => {
|
|
139
|
+
try {
|
|
140
|
+
return TextResult(await api.request(`/users/${id}/2fa`, { method: "DELETE" }));
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
return ErrorResult(e);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
server.tool("users_2fa_backup_codes", "Generate new 2FA backup codes (POST /users/{id}/2fa/backup-codes).", { id: z.union([z.number().int(), z.literal("me")]) }, async ({ id }) => {
|
|
147
|
+
try {
|
|
148
|
+
return TextResult(await api.request(`/users/${id}/2fa/backup-codes`, { method: "POST" }));
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
return ErrorResult(e);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const TextResult = (value) => ({
|
|
3
|
+
content: [
|
|
4
|
+
{
|
|
5
|
+
type: "text",
|
|
6
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
7
|
+
},
|
|
8
|
+
],
|
|
9
|
+
});
|
|
10
|
+
export const ErrorResult = (err) => ({
|
|
11
|
+
isError: true,
|
|
12
|
+
content: [
|
|
13
|
+
{
|
|
14
|
+
type: "text",
|
|
15
|
+
text: err instanceof Error ? err.message : String(err),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
export const IdParam = { id: z.number().int().describe("Resource ID") };
|
|
20
|
+
export const ListExpand = {
|
|
21
|
+
expand: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Comma-separated expansions (depends on endpoint). Examples: owner, access_list, certificate, clients, items"),
|
|
25
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warnyin/nginx-proxy-manager-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing the full Nginx Proxy Manager API as tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nginx-proxy-manager-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/warnyin/nginx-proxy-manager-mcp.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/warnyin/nginx-proxy-manager-mcp#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/warnyin/nginx-proxy-manager-mcp/issues"
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"prepare": "tsc",
|
|
29
|
+
"start": "node dist/index.js",
|
|
30
|
+
"dev": "tsx src/index.ts",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"model-context-protocol",
|
|
36
|
+
"nginx-proxy-manager",
|
|
37
|
+
"npm",
|
|
38
|
+
"claude"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
46
|
+
"zod": "^3.23.8"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.7.5",
|
|
50
|
+
"tsx": "^4.19.1",
|
|
51
|
+
"typescript": "^5.6.2"
|
|
52
|
+
}
|
|
53
|
+
}
|