@vibetools/dokploy-mcp 0.4.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/README.md +692 -0
- package/dist/api/client.d.ts +11 -0
- package/dist/api/client.js +103 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +48 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +112 -0
- package/dist/config/resolver.d.ts +38 -0
- package/dist/config/resolver.js +290 -0
- package/dist/config/types.d.ts +25 -0
- package/dist/config/types.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +17 -0
- package/dist/tools/_factory.d.ts +53 -0
- package/dist/tools/_factory.js +86 -0
- package/dist/tools/admin.d.ts +2 -0
- package/dist/tools/admin.js +61 -0
- package/dist/tools/application.d.ts +2 -0
- package/dist/tools/application.js +464 -0
- package/dist/tools/auth.d.ts +2 -0
- package/dist/tools/auth.js +150 -0
- package/dist/tools/backup.d.ts +2 -0
- package/dist/tools/backup.js +103 -0
- package/dist/tools/certificates.d.ts +2 -0
- package/dist/tools/certificates.js +54 -0
- package/dist/tools/cluster.d.ts +2 -0
- package/dist/tools/cluster.js +38 -0
- package/dist/tools/compose.d.ts +2 -0
- package/dist/tools/compose.js +213 -0
- package/dist/tools/deployment.d.ts +2 -0
- package/dist/tools/deployment.js +27 -0
- package/dist/tools/destination.d.ts +2 -0
- package/dist/tools/destination.js +78 -0
- package/dist/tools/docker.d.ts +2 -0
- package/dist/tools/docker.js +50 -0
- package/dist/tools/domain.d.ts +2 -0
- package/dist/tools/domain.js +134 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +48 -0
- package/dist/tools/mariadb.d.ts +2 -0
- package/dist/tools/mariadb.js +170 -0
- package/dist/tools/mongo.d.ts +2 -0
- package/dist/tools/mongo.js +168 -0
- package/dist/tools/mounts.d.ts +2 -0
- package/dist/tools/mounts.js +65 -0
- package/dist/tools/mysql.d.ts +2 -0
- package/dist/tools/mysql.js +170 -0
- package/dist/tools/port.d.ts +2 -0
- package/dist/tools/port.js +54 -0
- package/dist/tools/postgres.d.ts +2 -0
- package/dist/tools/postgres.js +169 -0
- package/dist/tools/project.d.ts +2 -0
- package/dist/tools/project.js +94 -0
- package/dist/tools/redirects.d.ts +2 -0
- package/dist/tools/redirects.js +53 -0
- package/dist/tools/redis.d.ts +2 -0
- package/dist/tools/redis.js +167 -0
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +81 -0
- package/dist/tools/security.d.ts +2 -0
- package/dist/tools/security.js +48 -0
- package/dist/tools/settings.d.ts +2 -0
- package/dist/tools/settings.js +258 -0
- package/dist/tools/user.d.ts +2 -0
- package/dist/tools/user.js +12 -0
- package/package.json +64 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly statusText: string;
|
|
4
|
+
readonly body: unknown;
|
|
5
|
+
readonly endpoint: string;
|
|
6
|
+
constructor(status: number, statusText: string, body: unknown, endpoint: string);
|
|
7
|
+
}
|
|
8
|
+
export declare const api: {
|
|
9
|
+
get: <T = unknown>(path: string, params?: Record<string, unknown>) => Promise<T>;
|
|
10
|
+
post: <T = unknown>(path: string, body?: unknown) => Promise<T>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { resolveConfig } from '../config/resolver.js';
|
|
2
|
+
function getConfig() {
|
|
3
|
+
const resolved = resolveConfig();
|
|
4
|
+
if (!resolved) {
|
|
5
|
+
throw new Error([
|
|
6
|
+
'Dokploy MCP is not configured. Set up credentials using one of these methods:',
|
|
7
|
+
'',
|
|
8
|
+
' 1. Run: npx @vibetools/dokploy-mcp setup',
|
|
9
|
+
' 2. Set environment variables: DOKPLOY_URL and DOKPLOY_API_KEY',
|
|
10
|
+
'',
|
|
11
|
+
'Get your API key from Dokploy Settings > API.',
|
|
12
|
+
].join('\n'));
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
baseUrl: resolved.url.replace(/\/+$/, ''),
|
|
16
|
+
apiKey: resolved.apiKey,
|
|
17
|
+
timeout: resolved.timeout,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
let _config = null;
|
|
21
|
+
function config() {
|
|
22
|
+
_config ??= getConfig();
|
|
23
|
+
return _config;
|
|
24
|
+
}
|
|
25
|
+
export class ApiError extends Error {
|
|
26
|
+
status;
|
|
27
|
+
statusText;
|
|
28
|
+
body;
|
|
29
|
+
endpoint;
|
|
30
|
+
constructor(status, statusText, body, endpoint) {
|
|
31
|
+
const msg = typeof body === 'object' && body !== null && 'message' in body
|
|
32
|
+
? body.message
|
|
33
|
+
: statusText;
|
|
34
|
+
super(`Dokploy API error (${status}): ${msg}`);
|
|
35
|
+
this.status = status;
|
|
36
|
+
this.statusText = statusText;
|
|
37
|
+
this.body = body;
|
|
38
|
+
this.endpoint = endpoint;
|
|
39
|
+
this.name = 'ApiError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function buildQueryString(body) {
|
|
43
|
+
if (!body || typeof body !== 'object') {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
for (const [k, v] of Object.entries(body)) {
|
|
48
|
+
if (v !== undefined && v !== null) {
|
|
49
|
+
params.set(k, String(v));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return params.toString();
|
|
53
|
+
}
|
|
54
|
+
function isAbortError(error) {
|
|
55
|
+
return error instanceof DOMException || (error instanceof Error && error.name === 'AbortError');
|
|
56
|
+
}
|
|
57
|
+
async function request(method, path, body) {
|
|
58
|
+
const { baseUrl, apiKey, timeout } = config();
|
|
59
|
+
const qs = method === 'GET' ? buildQueryString(body) : '';
|
|
60
|
+
const url = qs ? `${baseUrl}${path}?${qs}` : `${baseUrl}${path}`;
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(url, {
|
|
65
|
+
method,
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
Accept: 'application/json',
|
|
69
|
+
'x-api-key': apiKey,
|
|
70
|
+
},
|
|
71
|
+
body: method === 'POST' && body ? JSON.stringify(body) : undefined,
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
});
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
let data;
|
|
76
|
+
try {
|
|
77
|
+
data = JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
data = text;
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new ApiError(response.status, response.statusText, data, path);
|
|
84
|
+
}
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof ApiError) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
if (isAbortError(error)) {
|
|
92
|
+
throw new Error(`Request to ${path} timed out after ${timeout}ms`);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export const api = {
|
|
101
|
+
get: (path, params) => request('GET', path, params),
|
|
102
|
+
post: (path, body) => request('POST', path, body),
|
|
103
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCli(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export async function runCli(args) {
|
|
2
|
+
const command = args[0];
|
|
3
|
+
switch (command) {
|
|
4
|
+
case 'setup':
|
|
5
|
+
case 'init':
|
|
6
|
+
case 'auth': {
|
|
7
|
+
const { runSetup } = await import('./setup.js');
|
|
8
|
+
await runSetup();
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
case 'version':
|
|
12
|
+
case '--version':
|
|
13
|
+
case '-v': {
|
|
14
|
+
const { readFileSync } = await import('node:fs');
|
|
15
|
+
const { fileURLToPath } = await import('node:url');
|
|
16
|
+
const { dirname, join } = await import('node:path');
|
|
17
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(currentDir, '..', '..', 'package.json'), 'utf8'));
|
|
19
|
+
console.log(`@vibetools/dokploy-mcp v${pkg.version}`);
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
default:
|
|
23
|
+
printHelp();
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function printHelp() {
|
|
28
|
+
console.log(`
|
|
29
|
+
@vibetools/dokploy-mcp - MCP server for the Dokploy API
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
npx @vibetools/dokploy-mcp Start MCP server (stdio transport)
|
|
33
|
+
npx @vibetools/dokploy-mcp setup Configure credentials and MCP client
|
|
34
|
+
npx @vibetools/dokploy-mcp version Show version
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
setup, init, auth Interactive setup wizard
|
|
38
|
+
version, -v Show version number
|
|
39
|
+
|
|
40
|
+
Environment Variables:
|
|
41
|
+
DOKPLOY_URL Dokploy panel URL (e.g. https://panel.example.com)
|
|
42
|
+
DOKPLOY_API_KEY API key from Dokploy Settings
|
|
43
|
+
DOKPLOY_TIMEOUT Request timeout in ms (default: 30000)
|
|
44
|
+
|
|
45
|
+
Documentation:
|
|
46
|
+
https://github.com/vcode-sh/dokploy-mcp
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runSetup(): Promise<void>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { resolveConfig, saveConfig, validateCredentials } from '../config/resolver.js';
|
|
3
|
+
import { getConfigFilePath } from '../config/types.js';
|
|
4
|
+
async function promptCredentials() {
|
|
5
|
+
const url = await p.text({
|
|
6
|
+
message: 'Dokploy server URL',
|
|
7
|
+
placeholder: 'https://panel.example.com',
|
|
8
|
+
validate: (value) => {
|
|
9
|
+
if (!value?.trim())
|
|
10
|
+
return 'URL is required';
|
|
11
|
+
try {
|
|
12
|
+
new URL(value.trim());
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 'Please enter a valid URL (e.g. https://panel.example.com)';
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (p.isCancel(url)) {
|
|
20
|
+
p.cancel('Setup cancelled.');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const apiKey = await p.password({
|
|
24
|
+
message: 'API key (from Dokploy Settings > API)',
|
|
25
|
+
validate: (value) => {
|
|
26
|
+
if (!value?.trim())
|
|
27
|
+
return 'API key is required';
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (p.isCancel(apiKey)) {
|
|
31
|
+
p.cancel('Setup cancelled.');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
return { url: url.trim(), apiKey: apiKey.trim() };
|
|
35
|
+
}
|
|
36
|
+
const sourceLabels = {
|
|
37
|
+
env: 'environment variables',
|
|
38
|
+
'config-file': 'config file',
|
|
39
|
+
'dokploy-cli': 'Dokploy CLI config',
|
|
40
|
+
};
|
|
41
|
+
export async function runSetup() {
|
|
42
|
+
p.intro('@vibetools/dokploy-mcp setup');
|
|
43
|
+
// 1. Check for existing configuration
|
|
44
|
+
const existing = resolveConfig();
|
|
45
|
+
let url;
|
|
46
|
+
let apiKey;
|
|
47
|
+
if (existing) {
|
|
48
|
+
const sourceLabel = sourceLabels[existing.source] ?? existing.source;
|
|
49
|
+
const configFilePath = existing.source === 'config-file' ? ` (${getConfigFilePath()})` : '';
|
|
50
|
+
p.log.info(`Found existing credentials from ${sourceLabel}${configFilePath}`);
|
|
51
|
+
p.log.info(`URL: ${existing.url}`);
|
|
52
|
+
const useExisting = await p.confirm({
|
|
53
|
+
message: 'Use existing credentials?',
|
|
54
|
+
});
|
|
55
|
+
if (p.isCancel(useExisting)) {
|
|
56
|
+
p.cancel('Setup cancelled.');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (useExisting) {
|
|
60
|
+
url = existing.url;
|
|
61
|
+
apiKey = existing.apiKey;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const result = await promptCredentials();
|
|
65
|
+
url = result.url;
|
|
66
|
+
apiKey = result.apiKey;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
p.log.info('No existing configuration found.');
|
|
71
|
+
const result = await promptCredentials();
|
|
72
|
+
url = result.url;
|
|
73
|
+
apiKey = result.apiKey;
|
|
74
|
+
}
|
|
75
|
+
// 2. Validate credentials against the Dokploy API
|
|
76
|
+
const s = p.spinner();
|
|
77
|
+
s.start('Validating credentials...');
|
|
78
|
+
const validation = await validateCredentials(url, apiKey);
|
|
79
|
+
if (!validation.valid) {
|
|
80
|
+
s.stop('Validation failed');
|
|
81
|
+
p.log.error(validation.error ?? 'Could not connect to Dokploy server');
|
|
82
|
+
p.outro('Please check your URL and API key and try again.');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
s.stop('Credentials validated successfully');
|
|
86
|
+
if (validation.user)
|
|
87
|
+
p.log.success(`Authenticated as: ${validation.user}`);
|
|
88
|
+
if (validation.version)
|
|
89
|
+
p.log.success(`Dokploy version: ${validation.version}`);
|
|
90
|
+
// Use the normalized URL if the validator resolved one
|
|
91
|
+
if (validation.resolvedUrl)
|
|
92
|
+
url = validation.resolvedUrl;
|
|
93
|
+
// 3. Save config to disk
|
|
94
|
+
const configPath = saveConfig({ url, apiKey });
|
|
95
|
+
p.log.success(`Config saved to ${configPath}`);
|
|
96
|
+
// 4. Show MCP client configuration snippet
|
|
97
|
+
const mcpConfig = JSON.stringify({
|
|
98
|
+
mcpServers: {
|
|
99
|
+
dokploy: {
|
|
100
|
+
command: 'npx',
|
|
101
|
+
args: ['@vibetools/dokploy-mcp'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}, null, 2);
|
|
105
|
+
p.note(mcpConfig, 'Add to your MCP client config');
|
|
106
|
+
p.log.step('Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json');
|
|
107
|
+
p.log.step('Claude Code: .claude/settings.json or .mcp.json');
|
|
108
|
+
p.log.step('Cursor: ~/.cursor/mcp.json');
|
|
109
|
+
p.log.step('VS Code: .vscode/mcp.json');
|
|
110
|
+
// 5. Done
|
|
111
|
+
p.outro('Setup complete! Restart your MCP client to connect.');
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DokployConfig, ResolvedConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a Dokploy URL to the tRPC API base.
|
|
4
|
+
* Accepts any of these formats:
|
|
5
|
+
* https://panel.example.com
|
|
6
|
+
* https://panel.example.com/api
|
|
7
|
+
* https://panel.example.com/api/trpc
|
|
8
|
+
* Always returns https://panel.example.com/api/trpc
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeUrl(url: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves Dokploy configuration from multiple sources in priority order:
|
|
13
|
+
* 1. Environment variables (DOKPLOY_URL + DOKPLOY_API_KEY)
|
|
14
|
+
* 2. Config file (~/.config/dokploy-mcp/config.json)
|
|
15
|
+
* 3. Dokploy CLI config (@dokploy/cli global install)
|
|
16
|
+
*
|
|
17
|
+
* URLs are automatically normalized to the tRPC API base path.
|
|
18
|
+
* Returns null if no configuration is found.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveConfig(): ResolvedConfig | null;
|
|
21
|
+
/**
|
|
22
|
+
* Saves configuration to the config file.
|
|
23
|
+
* Creates the config directory if it doesn't exist.
|
|
24
|
+
* Returns the file path where the config was saved.
|
|
25
|
+
*/
|
|
26
|
+
export declare function saveConfig(config: DokployConfig): string;
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
resolvedUrl?: string;
|
|
30
|
+
user?: string;
|
|
31
|
+
version?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validates Dokploy credentials by making API requests.
|
|
36
|
+
* Tries to detect the correct URL format and validates the API key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateCredentials(url: string, apiKey: string): Promise<ValidationResult>;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getConfigDir, getConfigFilePath } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a Dokploy URL to the tRPC API base.
|
|
7
|
+
* Accepts any of these formats:
|
|
8
|
+
* https://panel.example.com
|
|
9
|
+
* https://panel.example.com/api
|
|
10
|
+
* https://panel.example.com/api/trpc
|
|
11
|
+
* Always returns https://panel.example.com/api/trpc
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeUrl(url) {
|
|
14
|
+
const stripped = url.replace(/\/+$/, '');
|
|
15
|
+
if (stripped.endsWith('/api/trpc'))
|
|
16
|
+
return stripped;
|
|
17
|
+
if (stripped.endsWith('/api'))
|
|
18
|
+
return `${stripped}/trpc`;
|
|
19
|
+
return `${stripped}/api/trpc`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves Dokploy configuration from multiple sources in priority order:
|
|
23
|
+
* 1. Environment variables (DOKPLOY_URL + DOKPLOY_API_KEY)
|
|
24
|
+
* 2. Config file (~/.config/dokploy-mcp/config.json)
|
|
25
|
+
* 3. Dokploy CLI config (@dokploy/cli global install)
|
|
26
|
+
*
|
|
27
|
+
* URLs are automatically normalized to the tRPC API base path.
|
|
28
|
+
* Returns null if no configuration is found.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveConfig() {
|
|
31
|
+
const timeout = Number.parseInt(process.env.DOKPLOY_TIMEOUT || '30000', 10);
|
|
32
|
+
// 1. Environment variables (highest priority)
|
|
33
|
+
const envUrl = process.env.DOKPLOY_URL;
|
|
34
|
+
const envApiKey = process.env.DOKPLOY_API_KEY;
|
|
35
|
+
if (envUrl && envApiKey) {
|
|
36
|
+
return {
|
|
37
|
+
url: normalizeUrl(envUrl),
|
|
38
|
+
apiKey: envApiKey,
|
|
39
|
+
source: 'env',
|
|
40
|
+
timeout,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// 2. Config file
|
|
44
|
+
const configFromFile = readConfigFile();
|
|
45
|
+
if (configFromFile) {
|
|
46
|
+
return {
|
|
47
|
+
url: normalizeUrl(configFromFile.url),
|
|
48
|
+
apiKey: configFromFile.apiKey,
|
|
49
|
+
source: 'config-file',
|
|
50
|
+
timeout,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// 3. Dokploy CLI config
|
|
54
|
+
const configFromCli = readDokployCliConfig();
|
|
55
|
+
if (configFromCli) {
|
|
56
|
+
return {
|
|
57
|
+
url: normalizeUrl(configFromCli.url),
|
|
58
|
+
apiKey: configFromCli.apiKey,
|
|
59
|
+
source: 'dokploy-cli',
|
|
60
|
+
timeout,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Reads the config file at the platform-appropriate location.
|
|
67
|
+
* Returns null if the file doesn't exist or is invalid.
|
|
68
|
+
*/
|
|
69
|
+
function readConfigFile() {
|
|
70
|
+
const filePath = getConfigFilePath();
|
|
71
|
+
if (!existsSync(filePath)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const content = readFileSync(filePath, 'utf8');
|
|
76
|
+
const parsed = JSON.parse(content);
|
|
77
|
+
if (typeof parsed !== 'object' ||
|
|
78
|
+
parsed === null ||
|
|
79
|
+
!('url' in parsed) ||
|
|
80
|
+
!('apiKey' in parsed)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const record = parsed;
|
|
84
|
+
if (typeof record.url !== 'string' || typeof record.apiKey !== 'string') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!(record.url && record.apiKey)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return { url: record.url, apiKey: record.apiKey };
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Reads the Dokploy CLI global config.
|
|
98
|
+
* The CLI stores { url, token } where url is the bare panel URL.
|
|
99
|
+
* Maps token to apiKey; URL normalization is handled by resolveConfig().
|
|
100
|
+
*/
|
|
101
|
+
function readDokployCliConfig() {
|
|
102
|
+
try {
|
|
103
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
104
|
+
const cliConfigPath = join(globalRoot, '@dokploy', 'cli', 'config.json');
|
|
105
|
+
if (!existsSync(cliConfigPath)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const content = readFileSync(cliConfigPath, 'utf8');
|
|
109
|
+
const parsed = JSON.parse(content);
|
|
110
|
+
if (typeof parsed !== 'object' ||
|
|
111
|
+
parsed === null ||
|
|
112
|
+
!('url' in parsed) ||
|
|
113
|
+
!('token' in parsed)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const record = parsed;
|
|
117
|
+
if (typeof record.url !== 'string' || typeof record.token !== 'string') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!(record.url && record.token)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return { url: record.url, apiKey: record.token };
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Saves configuration to the config file.
|
|
131
|
+
* Creates the config directory if it doesn't exist.
|
|
132
|
+
* Returns the file path where the config was saved.
|
|
133
|
+
*/
|
|
134
|
+
export function saveConfig(config) {
|
|
135
|
+
const configDir = getConfigDir();
|
|
136
|
+
const filePath = getConfigFilePath();
|
|
137
|
+
mkdirSync(configDir, { recursive: true });
|
|
138
|
+
const data = {
|
|
139
|
+
url: config.url,
|
|
140
|
+
apiKey: config.apiKey,
|
|
141
|
+
};
|
|
142
|
+
writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
143
|
+
return filePath;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Builds a list of candidate base URLs to try for validation.
|
|
147
|
+
* Handles bare panel URLs, /api, and /api/trpc suffixes.
|
|
148
|
+
*/
|
|
149
|
+
function buildCandidateUrls(url) {
|
|
150
|
+
const normalized = url.replace(/\/+$/, '');
|
|
151
|
+
if (normalized.endsWith('/api/trpc')) {
|
|
152
|
+
return [normalized];
|
|
153
|
+
}
|
|
154
|
+
if (normalized.endsWith('/api')) {
|
|
155
|
+
// User may have meant /api/trpc — try both
|
|
156
|
+
return [`${normalized}/trpc`, normalized];
|
|
157
|
+
}
|
|
158
|
+
// Bare panel URL — try the most common path first
|
|
159
|
+
return [`${normalized}/api/trpc`, `${normalized}/api`, normalized];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Validates Dokploy credentials by making API requests.
|
|
163
|
+
* Tries to detect the correct URL format and validates the API key.
|
|
164
|
+
*/
|
|
165
|
+
export async function validateCredentials(url, apiKey) {
|
|
166
|
+
const normalizedUrl = url.replace(/\/+$/, '');
|
|
167
|
+
const candidates = buildCandidateUrls(normalizedUrl);
|
|
168
|
+
for (const baseUrl of candidates) {
|
|
169
|
+
const result = await tryValidate(baseUrl, apiKey);
|
|
170
|
+
if (result.valid) {
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
// Auth error means the URL was right but the key was wrong — stop trying
|
|
174
|
+
if (result.error &&
|
|
175
|
+
!result.error.includes('not reachable') &&
|
|
176
|
+
!result.error.includes('Not Found')) {
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
valid: false,
|
|
182
|
+
error: `Could not connect to Dokploy at ${normalizedUrl}. Ensure the URL is correct and the server is running.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function apiHeaders(apiKey) {
|
|
186
|
+
return { Accept: 'application/json', 'x-api-key': apiKey };
|
|
187
|
+
}
|
|
188
|
+
function mapAuthError(status, statusText) {
|
|
189
|
+
if (status === 401 || status === 403) {
|
|
190
|
+
return { valid: false, error: 'Invalid API key. Check your key in Dokploy Settings > API.' };
|
|
191
|
+
}
|
|
192
|
+
if (status === 404) {
|
|
193
|
+
return { valid: false, error: 'Not Found' };
|
|
194
|
+
}
|
|
195
|
+
return { valid: false, error: `API returned HTTP ${status}: ${statusText}` };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Unwraps a tRPC response envelope: { result: { data: { json: T } } } → T
|
|
199
|
+
* Falls back to the raw data if it's not in tRPC format.
|
|
200
|
+
*/
|
|
201
|
+
function unwrapTrpc(data) {
|
|
202
|
+
if (typeof data !== 'object' || data === null)
|
|
203
|
+
return data;
|
|
204
|
+
const outer = data;
|
|
205
|
+
if (typeof outer.result !== 'object' || outer.result === null)
|
|
206
|
+
return data;
|
|
207
|
+
const result = outer.result;
|
|
208
|
+
if (typeof result.data !== 'object' || result.data === null)
|
|
209
|
+
return data;
|
|
210
|
+
const inner = result.data;
|
|
211
|
+
return 'json' in inner ? inner.json : data;
|
|
212
|
+
}
|
|
213
|
+
function parseUser(data) {
|
|
214
|
+
const unwrapped = unwrapTrpc(data);
|
|
215
|
+
if (typeof unwrapped !== 'object' || unwrapped === null)
|
|
216
|
+
return undefined;
|
|
217
|
+
const record = unwrapped;
|
|
218
|
+
// Top-level email/name
|
|
219
|
+
if (typeof record.email === 'string')
|
|
220
|
+
return record.email;
|
|
221
|
+
// Nested user object (tRPC user.get response)
|
|
222
|
+
if (typeof record.user === 'object' && record.user !== null) {
|
|
223
|
+
const user = record.user;
|
|
224
|
+
if (typeof user.email === 'string')
|
|
225
|
+
return user.email;
|
|
226
|
+
if (typeof user.firstName === 'string')
|
|
227
|
+
return user.firstName;
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
async function fetchVersion(baseUrl, apiKey) {
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
const timer = setTimeout(() => controller.abort(), 5_000);
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(`${baseUrl}/settings.getDokployVersion`, {
|
|
236
|
+
method: 'GET',
|
|
237
|
+
headers: apiHeaders(apiKey),
|
|
238
|
+
signal: controller.signal,
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok)
|
|
241
|
+
return undefined;
|
|
242
|
+
const data = await response.json();
|
|
243
|
+
const unwrapped = unwrapTrpc(data);
|
|
244
|
+
if (typeof unwrapped === 'string')
|
|
245
|
+
return unwrapped;
|
|
246
|
+
if (typeof unwrapped === 'object' && unwrapped !== null) {
|
|
247
|
+
const record = unwrapped;
|
|
248
|
+
if (typeof record.version === 'string')
|
|
249
|
+
return record.version;
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function tryValidate(baseUrl, apiKey) {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
263
|
+
try {
|
|
264
|
+
// Use user.get — the standard Dokploy tRPC endpoint for current user
|
|
265
|
+
const authResponse = await fetch(`${baseUrl}/user.get`, {
|
|
266
|
+
method: 'GET',
|
|
267
|
+
headers: apiHeaders(apiKey),
|
|
268
|
+
signal: controller.signal,
|
|
269
|
+
});
|
|
270
|
+
if (!authResponse.ok) {
|
|
271
|
+
return mapAuthError(authResponse.status, authResponse.statusText);
|
|
272
|
+
}
|
|
273
|
+
const authData = await authResponse.json();
|
|
274
|
+
const user = parseUser(authData);
|
|
275
|
+
const version = await fetchVersion(baseUrl, apiKey);
|
|
276
|
+
return { valid: true, resolvedUrl: baseUrl, user, version };
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (error instanceof DOMException || (error instanceof Error && error.name === 'AbortError')) {
|
|
280
|
+
return { valid: false, error: `Server at ${baseUrl} is not reachable (request timed out).` };
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
valid: false,
|
|
284
|
+
error: `Server at ${baseUrl} is not reachable: ${error instanceof Error ? error.message : String(error)}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface DokployConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
}
|
|
5
|
+
export type ConfigSource = 'env' | 'config-file' | 'dokploy-cli';
|
|
6
|
+
export interface ResolvedConfig extends DokployConfig {
|
|
7
|
+
source: ConfigSource;
|
|
8
|
+
timeout: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ConfigFile {
|
|
11
|
+
url: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the platform-appropriate config directory for dokploy-mcp.
|
|
16
|
+
*
|
|
17
|
+
* - macOS: ~/.config/dokploy-mcp
|
|
18
|
+
* - Linux: $XDG_CONFIG_HOME/dokploy-mcp or ~/.config/dokploy-mcp
|
|
19
|
+
* - Windows: %APPDATA%/dokploy-mcp
|
|
20
|
+
*/
|
|
21
|
+
export declare function getConfigDir(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the full path to the config file.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getConfigFilePath(): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { homedir, platform } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the platform-appropriate config directory for dokploy-mcp.
|
|
5
|
+
*
|
|
6
|
+
* - macOS: ~/.config/dokploy-mcp
|
|
7
|
+
* - Linux: $XDG_CONFIG_HOME/dokploy-mcp or ~/.config/dokploy-mcp
|
|
8
|
+
* - Windows: %APPDATA%/dokploy-mcp
|
|
9
|
+
*/
|
|
10
|
+
export function getConfigDir() {
|
|
11
|
+
const os = platform();
|
|
12
|
+
if (os === 'win32') {
|
|
13
|
+
const appData = process.env.APPDATA;
|
|
14
|
+
if (appData) {
|
|
15
|
+
return join(appData, 'dokploy-mcp');
|
|
16
|
+
}
|
|
17
|
+
return join(homedir(), 'AppData', 'Roaming', 'dokploy-mcp');
|
|
18
|
+
}
|
|
19
|
+
if (os === 'linux') {
|
|
20
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
21
|
+
if (xdgConfig) {
|
|
22
|
+
return join(xdgConfig, 'dokploy-mcp');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// macOS and Linux fallback
|
|
26
|
+
return join(homedir(), '.config', 'dokploy-mcp');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the full path to the config file.
|
|
30
|
+
*/
|
|
31
|
+
export function getConfigFilePath() {
|
|
32
|
+
return join(getConfigDir(), 'config.json');
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED