hamravesh-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/.env.example +19 -0
- package/CHANGELOG.md +16 -0
- package/ENDPOINTS-RAW.txt +578 -0
- package/ENDPOINTS.md +245 -0
- package/LICENSE +21 -0
- package/LIVE-STATUS.md +79 -0
- package/README.md +188 -0
- package/WRITE-ENDPOINTS.md +264 -0
- package/package.json +58 -0
- package/src/client.js +181 -0
- package/src/index.js +546 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* Hamravesh MCP server — کنترل کنسول همروش از داخل Claude بدون ورود به پنل.
|
|
5
|
+
*
|
|
6
|
+
* پیکربندی از طریق متغیرهای محیطی:
|
|
7
|
+
* HAMRAVESH_API_KEY کلید API همروش (توصیهشده)
|
|
8
|
+
* HAMRAVESH_EMAIL (جایگزین) ایمیل حساب
|
|
9
|
+
* HAMRAVESH_PASSWORD (جایگزین) رمز حساب
|
|
10
|
+
* HAMRAVESH_OTP (اختیاری) کد دومرحلهای
|
|
11
|
+
* HAMRAVESH_ORG سازمان پیشفرض (مثل wgcup)
|
|
12
|
+
* HAMRAVESH_BASE (اختیاری) base URL، پیشفرض https://api.hamravesh.com
|
|
13
|
+
* HAMRAVESH_ALLOW_WRITE = "1" تا ابزارهای تغییردهنده فعال شوند (پیشفرض: خاموش)
|
|
14
|
+
* HAMRAVESH_ALLOW_DELETE= "1" تا ابزارهای حذف فعال شوند (پیشفرض: خاموش)
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import {
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { HamraveshClient, HamraveshError } from "./client.js";
|
|
24
|
+
|
|
25
|
+
// --- بارگذاری اختیاری .env برای تست لوکال ---
|
|
26
|
+
try {
|
|
27
|
+
const env = readFileSync(new URL("../.env", import.meta.url), "utf8");
|
|
28
|
+
if (env.length <= 256 * 1024) {
|
|
29
|
+
for (const line of env.split("\n")) {
|
|
30
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
31
|
+
if (m && process.env[m[1]] === undefined) {
|
|
32
|
+
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
/* .env نبود، مهم نیست */
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- نسخه و راهنمای خط فرمان / version + CLI help ---
|
|
41
|
+
const PKG = (() => {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
44
|
+
} catch {
|
|
45
|
+
return { version: "?" };
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
const HELP = `hamravesh-mcp v${PKG.version} — MCP server for the Hamravesh / Darkube console.
|
|
50
|
+
|
|
51
|
+
This is an MCP server — it normally runs under an MCP client (Claude, Cursor, VS Code…),
|
|
52
|
+
not by hand. Configure it with environment variables:
|
|
53
|
+
|
|
54
|
+
HAMRAVESH_API_KEY your Hamravesh API key (Org settings → Security → API Keys)
|
|
55
|
+
HAMRAVESH_EMAIL / HAMRAVESH_PASSWORD alternative login (+ HAMRAVESH_OTP if 2FA)
|
|
56
|
+
HAMRAVESH_ORG default organization name
|
|
57
|
+
HAMRAVESH_ALLOW_WRITE=1 enable write tools (off by default)
|
|
58
|
+
HAMRAVESH_ALLOW_DELETE=1 enable delete tools (off by default)
|
|
59
|
+
|
|
60
|
+
Add to Claude Code in one line:
|
|
61
|
+
claude mcp add hamravesh --env HAMRAVESH_API_KEY=YOUR_KEY --env HAMRAVESH_ORG=YOUR_ORG -- npx -y hamravesh-mcp
|
|
62
|
+
|
|
63
|
+
Docs: https://github.com/bakhtarimohammad/hamravesh-mcp
|
|
64
|
+
──────────────────────────────────────────────────────────────────────────
|
|
65
|
+
این یک سرور MCP است و معمولاً زیرِ یک کلاینتِ MCP (مثل Claude) اجرا میشود، نه دستی.
|
|
66
|
+
با متغیرهای محیطیِ بالا تنظیمش کن. کلیدِ API را از پنل بساز: تنظیماتِ سازمان ← امنیت ← API Keys.
|
|
67
|
+
یا سادهتر: این را به Claude بگو «MCP همروش را برایم نصب کن، کلیدم این است: …».`;
|
|
68
|
+
|
|
69
|
+
const argv = process.argv.slice(2);
|
|
70
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
71
|
+
console.log(HELP);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
75
|
+
console.log(PKG.version);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const CFG = {
|
|
80
|
+
allowWrite: process.env.HAMRAVESH_ALLOW_WRITE === "1" || process.env.HAMRAVESH_ALLOW_WRITE === "true",
|
|
81
|
+
allowDelete: process.env.HAMRAVESH_ALLOW_DELETE === "1" || process.env.HAMRAVESH_ALLOW_DELETE === "true",
|
|
82
|
+
org: process.env.HAMRAVESH_ORG || "",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let client;
|
|
86
|
+
try {
|
|
87
|
+
client = new HamraveshClient({
|
|
88
|
+
apiKey: process.env.HAMRAVESH_API_KEY,
|
|
89
|
+
email: process.env.HAMRAVESH_EMAIL,
|
|
90
|
+
password: process.env.HAMRAVESH_PASSWORD,
|
|
91
|
+
otp: process.env.HAMRAVESH_OTP,
|
|
92
|
+
org: process.env.HAMRAVESH_ORG,
|
|
93
|
+
base: process.env.HAMRAVESH_BASE,
|
|
94
|
+
});
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// پیامِ مهربان: اگر کسی بدونِ تنظیمات اجراش کرد، راهنما را نشان بده (نه ارورِ خشک)
|
|
97
|
+
console.error("hamravesh-mcp: " + (e?.message || e) + "\n");
|
|
98
|
+
console.error(HELP);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- کمکیها ---
|
|
103
|
+
const MAX_TEXT = 200_000; // سقف حجم پاسخ برای جلوگیری از پرشدن context
|
|
104
|
+
/** رازها را از متن پاک میکند تا در پیام خطا لو نروند. */
|
|
105
|
+
function redact(s) {
|
|
106
|
+
let out = String(s ?? "");
|
|
107
|
+
for (const k of ["HAMRAVESH_API_KEY", "HAMRAVESH_PASSWORD", "HAMRAVESH_OTP"]) {
|
|
108
|
+
const v = process.env[k];
|
|
109
|
+
if (v) out = out.split(v).join("***");
|
|
110
|
+
}
|
|
111
|
+
return out.replace(/(Api-Key|Bearer)\s+[A-Za-z0-9._-]+/g, "$1 ***");
|
|
112
|
+
}
|
|
113
|
+
const T = (text) => {
|
|
114
|
+
let s = redact(text);
|
|
115
|
+
if (s.length > MAX_TEXT) s = s.slice(0, MAX_TEXT) + `\n…(بریده شد؛ ${s.length} کاراکتر بود)`;
|
|
116
|
+
return { content: [{ type: "text", text: s }] };
|
|
117
|
+
};
|
|
118
|
+
const J = (obj) => T(typeof obj === "string" ? obj : JSON.stringify(obj, null, 2));
|
|
119
|
+
const ERR = (text) => ({ content: [{ type: "text", text: redact(text) }], isError: true });
|
|
120
|
+
|
|
121
|
+
const APP_FIELDS =
|
|
122
|
+
"id,name,namespace,cluster,state,creation_method,custom_domain_address," +
|
|
123
|
+
"ram_limit,cpu_request,replicas,is_enabled,enable_SSL,image_repo,image_tag," +
|
|
124
|
+
"git_repo_url,git_branch_name,is_deployable,type";
|
|
125
|
+
|
|
126
|
+
/** @type {Array<{name:string,description:string,inputSchema:any,write?:boolean,destructive?:boolean,handler:(a:any)=>Promise<any>}>} */
|
|
127
|
+
const TOOLS = [
|
|
128
|
+
// ---------- خواندنی ----------
|
|
129
|
+
{
|
|
130
|
+
name: "hamravesh_whoami",
|
|
131
|
+
description: "پروفایل کاربر + فهرست سازمانها، نقشها، بودجه و کلاسترها.",
|
|
132
|
+
inputSchema: { type: "object", properties: {} },
|
|
133
|
+
handler: async () => {
|
|
134
|
+
const { body } = await client.get("/api/v2/users/profile");
|
|
135
|
+
const orgs = (body.organizations || []).map((o) => ({
|
|
136
|
+
id: o.id,
|
|
137
|
+
name: o.name,
|
|
138
|
+
roles: o.current_user_roles,
|
|
139
|
+
budget: o.budget,
|
|
140
|
+
is_restricted: o.is_restricted,
|
|
141
|
+
}));
|
|
142
|
+
return J({ full_name: body.full_name, email: body.email, organizations: orgs });
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "hamravesh_list_apps",
|
|
147
|
+
description: "فهرست اپهای دارکوب یک سازمان (با وضعیت، دامنه، منابع، replicas).",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: { org: { type: "string", description: "نام سازمان؛ پیشفرض از تنظیمات" } },
|
|
151
|
+
},
|
|
152
|
+
handler: async (a) => {
|
|
153
|
+
const { body } = await client.get("/api/v1/darkube/apps/", {
|
|
154
|
+
org: a.org,
|
|
155
|
+
params: { limit: 555, offset: 0, fields: APP_FIELDS },
|
|
156
|
+
});
|
|
157
|
+
return J(body.results || body);
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "hamravesh_get_app",
|
|
162
|
+
description: "جزئیات کامل یک اپ دارکوب (env، دامنه، منابع، بیلد، وضعیت).",
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: { app_id: { type: "string" }, org: { type: "string" } },
|
|
166
|
+
required: ["app_id"],
|
|
167
|
+
},
|
|
168
|
+
handler: async (a) => J((await client.get(`/api/v1/darkube/apps/${a.app_id}/`, { org: a.org })).body),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "hamravesh_app_containers",
|
|
172
|
+
description: "فهرست پادها/کانتینرهای یک اپ.",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: { app_id: { type: "string" }, org: { type: "string" } },
|
|
176
|
+
required: ["app_id"],
|
|
177
|
+
},
|
|
178
|
+
handler: async (a) =>
|
|
179
|
+
J((await client.get(`/api/v1/darkube/apps/${a.app_id}/app_containers/`, { org: a.org })).body),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "hamravesh_app_logs",
|
|
183
|
+
description: "لاگهای اخیر یک اپ. میتوان pod_name و container_name داد.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: {
|
|
187
|
+
app_id: { type: "string" },
|
|
188
|
+
pod_name: { type: "string" },
|
|
189
|
+
container_name: { type: "string" },
|
|
190
|
+
previous: { type: "boolean", description: "لاگ نمونهی قبلی (کرششده)" },
|
|
191
|
+
org: { type: "string" },
|
|
192
|
+
},
|
|
193
|
+
required: ["app_id"],
|
|
194
|
+
},
|
|
195
|
+
handler: async (a) =>
|
|
196
|
+
J(
|
|
197
|
+
(
|
|
198
|
+
await client.get(`/api/v1/darkube/apps/${a.app_id}/app_log/`, {
|
|
199
|
+
org: a.org,
|
|
200
|
+
params: { pod_name: a.pod_name, container_name: a.container_name, previous: a.previous },
|
|
201
|
+
})
|
|
202
|
+
).body
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "hamravesh_app_manifests",
|
|
207
|
+
description: "منیفستهای Kubernetes یک اپ (YAML).",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: { app_id: { type: "string" }, org: { type: "string" } },
|
|
211
|
+
required: ["app_id"],
|
|
212
|
+
},
|
|
213
|
+
handler: async (a) =>
|
|
214
|
+
J((await client.get(`/api/v1/darkube/apps/${a.app_id}/manifests/`, { org: a.org })).body),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "hamravesh_app_builds",
|
|
218
|
+
description: "تاریخچهی بیلدهای یک اپ.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: { app_id: { type: "string" }, limit: { type: "number" }, org: { type: "string" } },
|
|
222
|
+
required: ["app_id"],
|
|
223
|
+
},
|
|
224
|
+
handler: async (a) =>
|
|
225
|
+
J(
|
|
226
|
+
(
|
|
227
|
+
await client.get(`/api/v1/darkube/build/app/${a.app_id}/`, {
|
|
228
|
+
org: a.org,
|
|
229
|
+
params: { limit: Math.min(Math.max(Number(a.limit) || 10, 1), 200), offset: 0 },
|
|
230
|
+
})
|
|
231
|
+
).body
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "hamravesh_list_databases",
|
|
236
|
+
description: "فهرست دیتابیسهای مدیریتشده (DBaaS) یک سازمان.",
|
|
237
|
+
inputSchema: { type: "object", properties: { org: { type: "string" } } },
|
|
238
|
+
handler: async (a) => J((await client.get("/dbaas/api/v1/app/database/", { org: a.org })).body),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "hamravesh_list_registries",
|
|
242
|
+
description: "فهرست کانتینر رجیستریها و مصرف فضای آنها.",
|
|
243
|
+
inputSchema: { type: "object", properties: { org: { type: "string" } } },
|
|
244
|
+
handler: async (a) => J((await client.get("/api/v1/registry/", { org: a.org })).body),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "hamravesh_billing",
|
|
248
|
+
description: "وضعیت مالی سازمان: کیفپولها، بودجه و بدهی.",
|
|
249
|
+
inputSchema: { type: "object", properties: { org: { type: "string" } } },
|
|
250
|
+
handler: async (a) => {
|
|
251
|
+
const org = a.org || CFG.org;
|
|
252
|
+
const prof = (await client.get("/api/v2/users/profile")).body;
|
|
253
|
+
const o = (prof.organizations || []).find((x) => x.name === org) || {};
|
|
254
|
+
const wallets = (await client.get("/api/v1/billing/wallets/", { org }).catch(() => ({ body: null }))).body;
|
|
255
|
+
let account = null;
|
|
256
|
+
if (o.billing_account_id) {
|
|
257
|
+
account = (
|
|
258
|
+
await client
|
|
259
|
+
.get(`/api/v1/billing/billing-accounts/${o.billing_account_id}/`, { org })
|
|
260
|
+
.catch(() => ({ body: null }))
|
|
261
|
+
).body;
|
|
262
|
+
}
|
|
263
|
+
return J({ org, budget: o.budget, is_restricted: o.is_restricted, account, wallets });
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "hamravesh_list_apikeys",
|
|
268
|
+
description: "فهرست کلیدهای API سازمان.",
|
|
269
|
+
inputSchema: { type: "object", properties: { org: { type: "string" } } },
|
|
270
|
+
handler: async (a) => J((await client.get("/api/v1/apikeys/", { org: a.org })).body),
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// ---------- نوشتنی (نیازمند ALLOW_WRITE) ----------
|
|
274
|
+
{
|
|
275
|
+
name: "hamravesh_restart_app",
|
|
276
|
+
description: "ریاستارت یک اپ (همهی پادها دوباره راه میافتند).",
|
|
277
|
+
write: true,
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: { app_id: { type: "string" }, org: { type: "string" } },
|
|
281
|
+
required: ["app_id"],
|
|
282
|
+
},
|
|
283
|
+
handler: async (a) =>
|
|
284
|
+
J((await client.request("POST", `/api/v1/darkube/apps/${a.app_id}/restart/`, { org: a.org, body: {} })).body),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "hamravesh_redeploy_app",
|
|
288
|
+
description: "دیپلوی مجدد اپ از روی آخرین کامیت/بیلد.",
|
|
289
|
+
write: true,
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: { app_id: { type: "string" }, org: { type: "string" } },
|
|
293
|
+
required: ["app_id"],
|
|
294
|
+
},
|
|
295
|
+
handler: async (a) =>
|
|
296
|
+
J(
|
|
297
|
+
(
|
|
298
|
+
await client.get("/api/v1/darkube/build/build_last_commit/", {
|
|
299
|
+
org: a.org,
|
|
300
|
+
params: { app_id: a.app_id, deploy: true },
|
|
301
|
+
})
|
|
302
|
+
).body
|
|
303
|
+
),
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "hamravesh_scale_app",
|
|
307
|
+
description: "تغییر تعداد replica های یک اپ.",
|
|
308
|
+
write: true,
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
app_id: { type: "string" },
|
|
313
|
+
replicas: { type: "number" },
|
|
314
|
+
org: { type: "string" },
|
|
315
|
+
},
|
|
316
|
+
required: ["app_id", "replicas"],
|
|
317
|
+
},
|
|
318
|
+
handler: async (a) =>
|
|
319
|
+
J(
|
|
320
|
+
(
|
|
321
|
+
await client.request("PATCH", `/api/v1/darkube/apps/${a.app_id}/`, {
|
|
322
|
+
org: a.org,
|
|
323
|
+
body: { fields: ["replicas"], replicas: a.replicas },
|
|
324
|
+
})
|
|
325
|
+
).body
|
|
326
|
+
),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "hamravesh_set_app_enabled",
|
|
330
|
+
description: "روشن/خاموش کردن یک اپ (is_enabled).",
|
|
331
|
+
write: true,
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
app_id: { type: "string" },
|
|
336
|
+
enabled: { type: "boolean" },
|
|
337
|
+
org: { type: "string" },
|
|
338
|
+
},
|
|
339
|
+
required: ["app_id", "enabled"],
|
|
340
|
+
},
|
|
341
|
+
handler: async (a) =>
|
|
342
|
+
J(
|
|
343
|
+
(
|
|
344
|
+
await client.request("PATCH", `/api/v1/darkube/apps/${a.app_id}/`, {
|
|
345
|
+
org: a.org,
|
|
346
|
+
body: { fields: ["is_enabled"], is_enabled: a.enabled },
|
|
347
|
+
})
|
|
348
|
+
).body
|
|
349
|
+
),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "hamravesh_update_app_envs",
|
|
353
|
+
description:
|
|
354
|
+
"بهروزرسانی متغیرهای محیطی (env) یک اپ. ⚠️ ابتدا با hamravesh_get_app شکل فعلی envs را ببین؛ این کل آرایه را جایگزین میکند.",
|
|
355
|
+
write: true,
|
|
356
|
+
inputSchema: {
|
|
357
|
+
type: "object",
|
|
358
|
+
properties: {
|
|
359
|
+
app_id: { type: "string" },
|
|
360
|
+
envs: { type: "array", items: { type: "object" }, description: "آرایهی کامل env ها" },
|
|
361
|
+
org: { type: "string" },
|
|
362
|
+
},
|
|
363
|
+
required: ["app_id", "envs"],
|
|
364
|
+
},
|
|
365
|
+
handler: async (a) =>
|
|
366
|
+
J(
|
|
367
|
+
(
|
|
368
|
+
await client.request("PATCH", `/api/v1/darkube/apps/${a.app_id}/`, {
|
|
369
|
+
org: a.org,
|
|
370
|
+
body: { fields: ["envs"], envs: a.envs },
|
|
371
|
+
})
|
|
372
|
+
).body
|
|
373
|
+
),
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "hamravesh_create_app",
|
|
377
|
+
description:
|
|
378
|
+
"ساخت یک اپِ داکر-ایمیجِ جدید در دارکوب (بدنهاش با تست زنده تأیید شده). " +
|
|
379
|
+
"به plan (uuid پلن منابع)، namespace (id عددی namespace) و organization (id عددی سازمان) نیاز دارد — " +
|
|
380
|
+
"اینها را از hamravesh_get_app روی یک اپ موجود بگیر. مثال wgcup: organization=24772, namespace=165693, plan=73904144-6592-40ce-a96f-3789f611b5e4.",
|
|
381
|
+
write: true,
|
|
382
|
+
inputSchema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
name: { type: "string", description: "نام اپ (بعداً تغییرناپذیر)" },
|
|
386
|
+
image_repo: { type: "string", description: "آدرس ایمیج، مثل traefik/whoami" },
|
|
387
|
+
image_tag: { type: "string", description: "تگ ایمیج، مثل latest" },
|
|
388
|
+
service_port: { type: "number", description: "پورت سرویس/کانتینر، مثل 80" },
|
|
389
|
+
plan: { type: "string", description: "uuid پلن منابع" },
|
|
390
|
+
namespace: { type: "number", description: "id عددی namespace" },
|
|
391
|
+
organization: { type: "number", description: "id عددی سازمان" },
|
|
392
|
+
replicas: { type: "number", description: "پیشفرض 1" },
|
|
393
|
+
org: { type: "string", description: "نام سازمان برای هدر X-Organization" },
|
|
394
|
+
},
|
|
395
|
+
required: ["name", "image_repo", "image_tag", "service_port", "plan", "namespace", "organization"],
|
|
396
|
+
},
|
|
397
|
+
handler: async (a) => {
|
|
398
|
+
const port = Number(a.service_port);
|
|
399
|
+
const body = {
|
|
400
|
+
image_repo: a.image_repo,
|
|
401
|
+
image_tag: a.image_tag,
|
|
402
|
+
builder: "dockerfile",
|
|
403
|
+
creation_method: "docker_image",
|
|
404
|
+
name: a.name,
|
|
405
|
+
svc: { type: "ClusterIP", ports: { main: { protocol: "TCP", servicePort: port, containerPort: port } } },
|
|
406
|
+
command: "",
|
|
407
|
+
args: "",
|
|
408
|
+
readiness_probe_path: "",
|
|
409
|
+
custom_config: {},
|
|
410
|
+
plan: a.plan,
|
|
411
|
+
replicas: Number(a.replicas) || 1,
|
|
412
|
+
backup_config: null,
|
|
413
|
+
namespace: Number(a.namespace),
|
|
414
|
+
deploy_context: null,
|
|
415
|
+
ssl_challenge_type: "dns01",
|
|
416
|
+
organization: Number(a.organization),
|
|
417
|
+
};
|
|
418
|
+
return J((await client.request("POST", "/api/v1/darkube/apps/", { org: a.org, body })).body);
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "hamravesh_delete_app",
|
|
423
|
+
description: "حذفِ کاملِ یک اپ دارکوب (برگشتناپذیر). نیازمند ALLOW_DELETE.",
|
|
424
|
+
write: true,
|
|
425
|
+
destructive: true,
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: { app_id: { type: "string" }, org: { type: "string" } },
|
|
429
|
+
required: ["app_id"],
|
|
430
|
+
},
|
|
431
|
+
handler: async (a) => {
|
|
432
|
+
const { status, body } = await client.request("DELETE", `/api/v1/darkube/apps/${a.app_id}/`, { org: a.org });
|
|
433
|
+
return J(body !== undefined && body !== "" ? body : `حذف شد (HTTP ${status})`);
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "hamravesh_create_apikey",
|
|
438
|
+
description: "ساخت یک کلید API جدید برای سازمان. مقدار کلید فقط همین یک بار برمیگردد.",
|
|
439
|
+
write: true,
|
|
440
|
+
inputSchema: {
|
|
441
|
+
type: "object",
|
|
442
|
+
properties: { name: { type: "string" }, org: { type: "string" } },
|
|
443
|
+
required: ["name"],
|
|
444
|
+
},
|
|
445
|
+
handler: async (a) =>
|
|
446
|
+
J((await client.request("POST", "/api/v1/apikeys/", { org: a.org, body: { name: a.name } })).body),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "hamravesh_delete_apikey",
|
|
450
|
+
description: "حذف یک کلید API (با شناسهی id، نه مقدار value).",
|
|
451
|
+
write: true,
|
|
452
|
+
destructive: true,
|
|
453
|
+
inputSchema: {
|
|
454
|
+
type: "object",
|
|
455
|
+
properties: { id: { type: "string" }, org: { type: "string" } },
|
|
456
|
+
required: ["id"],
|
|
457
|
+
},
|
|
458
|
+
handler: async (a) => {
|
|
459
|
+
const { status, body } = await client.request("DELETE", `/api/v1/apikeys/${a.id}/`, { org: a.org });
|
|
460
|
+
return J(body !== undefined && body !== "" ? body : `حذف شد (HTTP ${status})`);
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
// ---------- جوکر: هر endpoint دلخواه ----------
|
|
465
|
+
{
|
|
466
|
+
name: "hamravesh_request",
|
|
467
|
+
description:
|
|
468
|
+
"درخواست خام به هر endpoint همروش (برای کارهایی که ابزار اختصاصی ندارند). " +
|
|
469
|
+
"GET همیشه مجاز است؛ POST/PUT/PATCH نیازمند ALLOW_WRITE و DELETE نیازمند ALLOW_DELETE است. " +
|
|
470
|
+
"فهرست کامل مسیرها در ENDPOINTS.md پروژه است.",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
type: "object",
|
|
473
|
+
properties: {
|
|
474
|
+
method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
475
|
+
path: { type: "string", description: "مثل /api/v1/darkube/plans/" },
|
|
476
|
+
org: { type: "string" },
|
|
477
|
+
params: { type: "object", description: "کوئریپارامترها" },
|
|
478
|
+
body: { type: "object", description: "بدنهی JSON برای متدهای نوشتنی" },
|
|
479
|
+
},
|
|
480
|
+
required: ["method", "path"],
|
|
481
|
+
},
|
|
482
|
+
handler: async (a) => {
|
|
483
|
+
const m = String(a.method).toUpperCase();
|
|
484
|
+
const path = String(a.path || "");
|
|
485
|
+
if (!path.startsWith("/"))
|
|
486
|
+
return ERR("path باید مسیر نسبی باشد و با / شروع شود (URL کامل مجاز نیست).");
|
|
487
|
+
const isWrite = m !== "GET";
|
|
488
|
+
const isDelete = m === "DELETE";
|
|
489
|
+
if (isWrite && !CFG.allowWrite)
|
|
490
|
+
return ERR(`متد ${m} نیازمند فعالبودن نوشتن است. HAMRAVESH_ALLOW_WRITE=1 را تنظیم کن.`);
|
|
491
|
+
if (isDelete && !CFG.allowDelete)
|
|
492
|
+
return ERR("DELETE نیازمند HAMRAVESH_ALLOW_DELETE=1 است.");
|
|
493
|
+
const { status, body } = await client.request(m, path, { org: a.org, params: a.params, body: a.body });
|
|
494
|
+
return J({ status, body });
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
const BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
500
|
+
|
|
501
|
+
const server = new Server(
|
|
502
|
+
{ name: "hamravesh-mcp", version: "0.1.0" },
|
|
503
|
+
{ capabilities: { tools: {} } }
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
507
|
+
tools: TOOLS.map((t) => {
|
|
508
|
+
let description = t.description;
|
|
509
|
+
if (t.write && !CFG.allowWrite) description += " (غیرفعال — برای فعالسازی HAMRAVESH_ALLOW_WRITE=1)";
|
|
510
|
+
return { name: t.name, description, inputSchema: t.inputSchema };
|
|
511
|
+
}),
|
|
512
|
+
}));
|
|
513
|
+
|
|
514
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
515
|
+
const tool = BY_NAME.get(req.params.name);
|
|
516
|
+
if (!tool) return ERR(`ابزار ناشناخته: ${req.params.name}`);
|
|
517
|
+
const args = req.params.arguments || {};
|
|
518
|
+
if (tool.write && !CFG.allowWrite)
|
|
519
|
+
return ERR(
|
|
520
|
+
`«${tool.name}» یک عملیات نوشتنی است و فعلاً خاموش است. برای فعالسازی HAMRAVESH_ALLOW_WRITE=1 را در تنظیمات MCP بگذار.`
|
|
521
|
+
);
|
|
522
|
+
if (tool.destructive && !CFG.allowDelete)
|
|
523
|
+
return ERR(`«${tool.name}» یک عملیات حذف است. برای فعالسازی HAMRAVESH_ALLOW_DELETE=1 را تنظیم کن.`);
|
|
524
|
+
try {
|
|
525
|
+
return await tool.handler(args);
|
|
526
|
+
} catch (e) {
|
|
527
|
+
if (e instanceof HamraveshError) return ERR(`خطای همروش — ${e.message}`);
|
|
528
|
+
return ERR(`خطا: ${e?.message || String(e)}`);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
process.on("unhandledRejection", (reason) => {
|
|
533
|
+
console.error("hamravesh-mcp: unhandledRejection —", redact(reason?.stack || reason));
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const transport = new StdioServerTransport();
|
|
538
|
+
await server.connect(transport);
|
|
539
|
+
console.error(
|
|
540
|
+
`hamravesh-mcp آماده است. auth=${client.authMode} org=${CFG.org || "(تعییننشده)"} ` +
|
|
541
|
+
`write=${CFG.allowWrite ? "on" : "off"} delete=${CFG.allowDelete ? "on" : "off"}`
|
|
542
|
+
);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
console.error("hamravesh-mcp: اتصال ناموفق —", redact(e?.message || String(e)));
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|