@francescomalatesta/laravel-forge-mcp 0.7.0 → 0.8.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 +9 -0
- package/dist/tools/databases/create-database-user.js +65 -0
- package/dist/tools/databases/create-database.js +63 -0
- package/dist/tools/databases/delete-database-user.js +40 -0
- package/dist/tools/databases/delete-database.js +40 -0
- package/dist/tools/databases/list-database-users.js +49 -0
- package/dist/tools/databases/list-databases.js +49 -0
- package/dist/tools/databases/shared.js +61 -0
- package/dist/tools/databases/sync-databases.js +28 -0
- package/dist/tools/databases/update-database-root-password.js +32 -0
- package/dist/tools/databases/update-database-user.js +39 -0
- package/dist/tools/registry.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,15 @@ npm run build
|
|
|
127
127
|
| `forge_set_php_opcache` | servers | Enable or disable OPcache |
|
|
128
128
|
| `forge_get_php_config` | servers | FPM or CLI php.ini, or the FPM pool configuration |
|
|
129
129
|
| `forge_update_php_config` | servers | Replace a PHP configuration file |
|
|
130
|
+
| `forge_list_databases` | databases | Database schemas on a server |
|
|
131
|
+
| `forge_create_database` | databases | Create a database (optionally with a user) and wait until installed |
|
|
132
|
+
| `forge_delete_database` | databases | Drop a database |
|
|
133
|
+
| `forge_sync_databases` | databases | Import databases created outside Forge |
|
|
134
|
+
| `forge_list_database_users` | databases | Database users on a server |
|
|
135
|
+
| `forge_create_database_user` | databases | Create a user with access to databases (by ID or name) |
|
|
136
|
+
| `forge_update_database_user` | databases | Change a user's password or databases |
|
|
137
|
+
| `forge_delete_database_user` | databases | Delete a database user |
|
|
138
|
+
| `forge_update_database_root_password` | databases | Change the database root password |
|
|
130
139
|
| `forge_list_deployments` | deployments | Deployments of a site or of every site on a server |
|
|
131
140
|
| `forge_get_deployment` | deployments | A deployment with the end of its log |
|
|
132
141
|
| `forge_get_deployment_status` | deployments | Whether a deployment is running |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { operationOutput, outcome, waitFor, waitInput } from '../shared/async.js';
|
|
5
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
6
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
7
|
+
import { databaseOutput, databasePhase, databaseRefsInput, databaseUsersPath, formatDatabase, passwordInput, readOne, resolveDatabaseIds, } from './shared.js';
|
|
8
|
+
const CHECK_WITH = 'forge_list_database_users';
|
|
9
|
+
export const createDatabaseUser = defineTool({
|
|
10
|
+
name: 'forge_create_database_user',
|
|
11
|
+
title: 'Create database user',
|
|
12
|
+
description: 'Create a database user, optionally with access to some databases (by ID or name) and read-only. Waits until it is installed unless `wait` is false.',
|
|
13
|
+
toolset: 'databases',
|
|
14
|
+
operations: [
|
|
15
|
+
'organizations.servers.database.users.store',
|
|
16
|
+
'organizations.servers.database.users.show',
|
|
17
|
+
'organizations.servers.database.schemas.index',
|
|
18
|
+
],
|
|
19
|
+
permissions: ['server:create-databases', 'server:view'],
|
|
20
|
+
readOnly: false,
|
|
21
|
+
destructive: false,
|
|
22
|
+
idempotent: false,
|
|
23
|
+
async: true,
|
|
24
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
25
|
+
inputSchema: {
|
|
26
|
+
organization: organizationInput,
|
|
27
|
+
server: serverInput,
|
|
28
|
+
name: z.string().min(1).describe('User name.'),
|
|
29
|
+
password: passwordInput,
|
|
30
|
+
databases: databaseRefsInput.optional(),
|
|
31
|
+
read_only: z.boolean().optional().describe('Grant read-only access.'),
|
|
32
|
+
...waitInput(120),
|
|
33
|
+
},
|
|
34
|
+
outputSchema: {
|
|
35
|
+
...operationOutput,
|
|
36
|
+
user: databaseOutput.nullable(),
|
|
37
|
+
},
|
|
38
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
39
|
+
const org = organization(args.organization);
|
|
40
|
+
const databaseIds = args.databases ? await resolveDatabaseIds(client, org, args.server, args.databases, signal) : undefined;
|
|
41
|
+
const base = databaseUsersPath(org, args.server);
|
|
42
|
+
const response = await client.post(base, {
|
|
43
|
+
body: { name: args.name, password: args.password, database_ids: databaseIds, read_only: args.read_only },
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
const initial = response.data?.data ? flattenSingle(response.data) : undefined;
|
|
47
|
+
const action = `create the database user ${args.name}`;
|
|
48
|
+
if (!initial || !args.wait) {
|
|
49
|
+
return {
|
|
50
|
+
structured: { status: 'queued', check_with: CHECK_WITH, user: initial ? formatDatabase(initial) : null },
|
|
51
|
+
summary: `Forge accepted the request to ${action}. Check it with ${CHECK_WITH}.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const result = await waitFor({
|
|
55
|
+
initial,
|
|
56
|
+
poll: () => readOne(client, `${base}/${encodeURIComponent(initial.id)}`, signal),
|
|
57
|
+
phase: (user) => databasePhase(user.status),
|
|
58
|
+
describe: (user) => `Database user ${user.name} is ${user.status}`,
|
|
59
|
+
timeoutSeconds: args.timeout_seconds,
|
|
60
|
+
context: { sleep, progress },
|
|
61
|
+
});
|
|
62
|
+
const done = outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
63
|
+
return { structured: { ...done.structured, user: formatDatabase(result.value ?? initial) }, summary: done.summary };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { ToolInputError } from '../errors.js';
|
|
5
|
+
import { operationOutput, outcome, waitFor, waitInput } from '../shared/async.js';
|
|
6
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
7
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
8
|
+
import { databaseOutput, databasePhase, databasesPath, formatDatabase, passwordInput, readOne } from './shared.js';
|
|
9
|
+
const CHECK_WITH = 'forge_list_databases';
|
|
10
|
+
export const createDatabase = defineTool({
|
|
11
|
+
name: 'forge_create_database',
|
|
12
|
+
title: 'Create database',
|
|
13
|
+
description: 'Create a database schema on a server, optionally with a new user that has access to it (`user` + `password`). Waits until it is installed unless `wait` is false. Connect it to a site by setting DB_* variables with forge_set_site_env_vars.',
|
|
14
|
+
toolset: 'databases',
|
|
15
|
+
operations: ['organizations.servers.database.schemas.store', 'organizations.servers.database.schemas.show'],
|
|
16
|
+
permissions: ['server:create-databases', 'server:view'],
|
|
17
|
+
readOnly: false,
|
|
18
|
+
destructive: false,
|
|
19
|
+
idempotent: false,
|
|
20
|
+
async: true,
|
|
21
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
22
|
+
inputSchema: {
|
|
23
|
+
organization: organizationInput,
|
|
24
|
+
server: serverInput,
|
|
25
|
+
name: z.string().min(1).describe('Database name.'),
|
|
26
|
+
user: z.string().min(1).optional().describe('Also create a user with access to this database.'),
|
|
27
|
+
password: passwordInput.optional().describe('Password of the new user (required with `user`, never returned).'),
|
|
28
|
+
backup_configuration_ids: z.array(z.number().int()).optional().describe('Backup configurations to add the database to.'),
|
|
29
|
+
...waitInput(120),
|
|
30
|
+
},
|
|
31
|
+
outputSchema: {
|
|
32
|
+
...operationOutput,
|
|
33
|
+
database: databaseOutput.nullable(),
|
|
34
|
+
},
|
|
35
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
36
|
+
if (Boolean(args.user) !== Boolean(args.password)) {
|
|
37
|
+
throw new ToolInputError('Pass both `user` and `password` to create a user with the database, or neither.');
|
|
38
|
+
}
|
|
39
|
+
const base = databasesPath(organization(args.organization), args.server);
|
|
40
|
+
const response = await client.post(base, {
|
|
41
|
+
body: { name: args.name, user: args.user, password: args.password, backup_configuration_ids: args.backup_configuration_ids },
|
|
42
|
+
signal,
|
|
43
|
+
});
|
|
44
|
+
const initial = response.data?.data ? flattenSingle(response.data) : undefined;
|
|
45
|
+
const action = `create the database ${args.name}${args.user ? ` with user ${args.user}` : ''}`;
|
|
46
|
+
if (!initial || !args.wait) {
|
|
47
|
+
return {
|
|
48
|
+
structured: { status: 'queued', check_with: CHECK_WITH, database: initial ? formatDatabase(initial) : null },
|
|
49
|
+
summary: `Forge accepted the request to ${action}. Check it with ${CHECK_WITH}.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const result = await waitFor({
|
|
53
|
+
initial,
|
|
54
|
+
poll: () => readOne(client, `${base}/${encodeURIComponent(initial.id)}`, signal),
|
|
55
|
+
phase: (database) => databasePhase(database.status),
|
|
56
|
+
describe: (database) => `Database ${database.name} is ${database.status}`,
|
|
57
|
+
timeoutSeconds: args.timeout_seconds,
|
|
58
|
+
context: { sleep, progress },
|
|
59
|
+
});
|
|
60
|
+
const done = outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
61
|
+
return { structured: { ...done.structured, database: formatDatabase(result.value ?? initial) }, summary: done.summary };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, orGone, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { databaseUserInput, databaseUsersPath, readOne } from './shared.js';
|
|
5
|
+
const CHECK_WITH = 'forge_list_database_users';
|
|
6
|
+
export const deleteDatabaseUser = defineTool({
|
|
7
|
+
name: 'forge_delete_database_user',
|
|
8
|
+
title: 'Delete database user',
|
|
9
|
+
description: 'Delete a database user. Applications connecting with it lose database access.',
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.users.destroy', 'organizations.servers.database.users.show'],
|
|
12
|
+
permissions: ['server:delete-databases', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: 'Check the user ID with forge_list_database_users.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
user: databaseUserInput,
|
|
22
|
+
...waitInput(120),
|
|
23
|
+
},
|
|
24
|
+
outputSchema: operationOutput,
|
|
25
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
26
|
+
const path = `${databaseUsersPath(organization(args.organization), args.server)}/${encodeURIComponent(String(args.user))}`;
|
|
27
|
+
await client.delete(path, { signal });
|
|
28
|
+
const action = `delete database user ${args.user}`;
|
|
29
|
+
if (!args.wait)
|
|
30
|
+
return queued(action, CHECK_WITH);
|
|
31
|
+
const result = await waitFor({
|
|
32
|
+
poll: () => orGone(() => readOne(client, path, signal)),
|
|
33
|
+
phase: (user) => (user === null ? 'completed' : 'pending'),
|
|
34
|
+
describe: (user) => `Database user is ${user?.status ?? 'removing'}`,
|
|
35
|
+
timeoutSeconds: args.timeout_seconds,
|
|
36
|
+
context: { sleep, progress },
|
|
37
|
+
});
|
|
38
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, orGone, outcome, queued, waitFor, waitInput } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { databaseInput, databasesPath, readOne } from './shared.js';
|
|
5
|
+
const CHECK_WITH = 'forge_list_databases';
|
|
6
|
+
export const deleteDatabase = defineTool({
|
|
7
|
+
name: 'forge_delete_database',
|
|
8
|
+
title: 'Delete database',
|
|
9
|
+
description: 'Drop a database schema from the server with all its data. This cannot be undone: make sure a backup exists.',
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.schemas.destroy', 'organizations.servers.database.schemas.show'],
|
|
12
|
+
permissions: ['server:delete-databases', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: 'Check the database ID with forge_list_databases.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
database: databaseInput,
|
|
22
|
+
...waitInput(120),
|
|
23
|
+
},
|
|
24
|
+
outputSchema: operationOutput,
|
|
25
|
+
async handler(args, { client, organization, signal, sleep, progress }) {
|
|
26
|
+
const path = `${databasesPath(organization(args.organization), args.server)}/${encodeURIComponent(String(args.database))}`;
|
|
27
|
+
await client.delete(path, { signal });
|
|
28
|
+
const action = `delete database ${args.database}`;
|
|
29
|
+
if (!args.wait)
|
|
30
|
+
return queued(action, CHECK_WITH);
|
|
31
|
+
const result = await waitFor({
|
|
32
|
+
poll: () => orGone(() => readOne(client, path, signal)),
|
|
33
|
+
phase: (database) => (database === null ? 'completed' : 'pending'),
|
|
34
|
+
describe: (database) => `Database is ${database?.status ?? 'removing'}`,
|
|
35
|
+
timeoutSeconds: args.timeout_seconds,
|
|
36
|
+
context: { sleep, progress },
|
|
37
|
+
});
|
|
38
|
+
return outcome(result, { action, checkWith: CHECK_WITH, timeoutSeconds: args.timeout_seconds });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { organizationInput, paginationInput, paginationOutput, paginationSummary, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { databaseOutput, databaseUserInput, databaseUsersPath, formatDatabase, readOne } from './shared.js';
|
|
7
|
+
const SORT = ['name', '-name', 'created_at', '-created_at', 'updated_at', '-updated_at'];
|
|
8
|
+
export const listDatabaseUsers = defineTool({
|
|
9
|
+
name: 'forge_list_database_users',
|
|
10
|
+
title: 'List database users',
|
|
11
|
+
description: 'List the database users on a server, or get one with `user`. Passwords and granted databases are not exposed by Forge.',
|
|
12
|
+
toolset: 'databases',
|
|
13
|
+
operations: ['organizations.servers.database.users.index', 'organizations.servers.database.users.show'],
|
|
14
|
+
permissions: ['server:view'],
|
|
15
|
+
readOnly: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
user: databaseUserInput.optional().describe('Return only this user.'),
|
|
21
|
+
name: z.string().min(1).optional().describe('Filter by name.'),
|
|
22
|
+
status: z.string().min(1).optional().describe('Filter by status, e.g. "installed".'),
|
|
23
|
+
sort: z.array(z.enum(SORT)).min(1).optional(),
|
|
24
|
+
...paginationInput,
|
|
25
|
+
},
|
|
26
|
+
outputSchema: {
|
|
27
|
+
users: z.array(databaseOutput),
|
|
28
|
+
...paginationOutput,
|
|
29
|
+
},
|
|
30
|
+
async handler(args, { client, organization, signal }) {
|
|
31
|
+
const base = databaseUsersPath(organization(args.organization), args.server);
|
|
32
|
+
if (args.user !== undefined) {
|
|
33
|
+
const user = formatDatabase(await readOne(client, `${base}/${encodeURIComponent(String(args.user))}`, signal));
|
|
34
|
+
return { structured: { users: [user], next_cursor: null, has_more: false }, summary: `Database user ${user.name} is ${user.status}.` };
|
|
35
|
+
}
|
|
36
|
+
const response = await client.get(base, {
|
|
37
|
+
query: { filter: { name: args.name, status: args.status }, sort: args.sort, page: { size: args.page_size, cursor: args.cursor } },
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
const page = flattenCollection(response.data);
|
|
41
|
+
const users = page.items.map(formatDatabase);
|
|
42
|
+
return {
|
|
43
|
+
structured: { users, next_cursor: page.nextCursor, has_more: page.nextCursor !== null },
|
|
44
|
+
summary: users.length === 0
|
|
45
|
+
? 'No database users found.'
|
|
46
|
+
: `Found ${users.length} database user(s): ${users.map((u) => u.name).join(', ')}.${paginationSummary(page.nextCursor)}`,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection } from '../../forge/jsonapi.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { organizationInput, paginationInput, paginationOutput, paginationSummary, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
6
|
+
import { databaseInput, databaseOutput, databasesPath, formatDatabase, readOne } from './shared.js';
|
|
7
|
+
const SORT = ['name', '-name', 'created_at', '-created_at', 'updated_at', '-updated_at'];
|
|
8
|
+
export const listDatabases = defineTool({
|
|
9
|
+
name: 'forge_list_databases',
|
|
10
|
+
title: 'List databases',
|
|
11
|
+
description: 'List the database schemas on a server (MySQL, MariaDB or Postgres), or get one with `database`.',
|
|
12
|
+
toolset: 'databases',
|
|
13
|
+
operations: ['organizations.servers.database.schemas.index', 'organizations.servers.database.schemas.show'],
|
|
14
|
+
permissions: ['server:view'],
|
|
15
|
+
readOnly: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
database: databaseInput.optional().describe('Return only this database.'),
|
|
21
|
+
name: z.string().min(1).optional().describe('Filter by name.'),
|
|
22
|
+
status: z.string().min(1).optional().describe('Filter by status, e.g. "installed".'),
|
|
23
|
+
sort: z.array(z.enum(SORT)).min(1).optional(),
|
|
24
|
+
...paginationInput,
|
|
25
|
+
},
|
|
26
|
+
outputSchema: {
|
|
27
|
+
databases: z.array(databaseOutput),
|
|
28
|
+
...paginationOutput,
|
|
29
|
+
},
|
|
30
|
+
async handler(args, { client, organization, signal }) {
|
|
31
|
+
const base = databasesPath(organization(args.organization), args.server);
|
|
32
|
+
if (args.database !== undefined) {
|
|
33
|
+
const database = formatDatabase(await readOne(client, `${base}/${encodeURIComponent(String(args.database))}`, signal));
|
|
34
|
+
return { structured: { databases: [database], next_cursor: null, has_more: false }, summary: `Database ${database.name} is ${database.status}.` };
|
|
35
|
+
}
|
|
36
|
+
const response = await client.get(base, {
|
|
37
|
+
query: { filter: { name: args.name, status: args.status }, sort: args.sort, page: { size: args.page_size, cursor: args.cursor } },
|
|
38
|
+
signal,
|
|
39
|
+
});
|
|
40
|
+
const page = flattenCollection(response.data);
|
|
41
|
+
const databases = page.items.map(formatDatabase);
|
|
42
|
+
return {
|
|
43
|
+
structured: { databases, next_cursor: page.nextCursor, has_more: page.nextCursor !== null },
|
|
44
|
+
summary: databases.length === 0
|
|
45
|
+
? 'No databases found.'
|
|
46
|
+
: `Found ${databases.length} database(s): ${databases.map((d) => d.name).join(', ')}.${paginationSummary(page.nextCursor)}`,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { flattenCollection, flattenSingle } from '../../forge/jsonapi.js';
|
|
3
|
+
import { ToolInputError } from '../errors.js';
|
|
4
|
+
import { phaseOf } from '../shared/async.js';
|
|
5
|
+
import { idInput, pick } from '../shared/schemas.js';
|
|
6
|
+
import { serverPath } from '../servers/shared.js';
|
|
7
|
+
export function databasesPath(org, server) {
|
|
8
|
+
return `${serverPath(org, server)}/database/schemas`;
|
|
9
|
+
}
|
|
10
|
+
export function databaseUsersPath(org, server) {
|
|
11
|
+
return `${serverPath(org, server)}/database/users`;
|
|
12
|
+
}
|
|
13
|
+
export const databaseInput = idInput('Database ID. Use forge_list_databases to find it.');
|
|
14
|
+
export const databaseUserInput = idInput('Database user ID. Use forge_list_database_users to find it.');
|
|
15
|
+
export const databaseRefsInput = z
|
|
16
|
+
.array(z.union([z.number().int().nonnegative(), z.string().min(1)]))
|
|
17
|
+
.describe('Databases by ID or by name, e.g. [12, "shop"].');
|
|
18
|
+
export const passwordInput = z.string().min(8).describe('Password (never returned by any tool).');
|
|
19
|
+
const FIELDS = ['id', 'name', 'status', 'created_at', 'updated_at'];
|
|
20
|
+
export const databaseOutput = z.looseObject({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
name: z.string().nullable(),
|
|
23
|
+
status: z.string().nullable().describe('installing, installed, removing (users also: updating).'),
|
|
24
|
+
created_at: z.string().nullable(),
|
|
25
|
+
updated_at: z.string().nullable(),
|
|
26
|
+
});
|
|
27
|
+
export function formatDatabase(flat) {
|
|
28
|
+
return pick(flat, FIELDS);
|
|
29
|
+
}
|
|
30
|
+
export async function readOne(client, path, signal) {
|
|
31
|
+
const response = await client.get(path, { signal });
|
|
32
|
+
return flattenSingle(response.data);
|
|
33
|
+
}
|
|
34
|
+
/** Databases and database users share the same transitional statuses. */
|
|
35
|
+
export function databasePhase(status) {
|
|
36
|
+
return phaseOf(status, { pending: ['installing', 'updating', 'removing'] });
|
|
37
|
+
}
|
|
38
|
+
/** Resolves database IDs or names to IDs, failing with the names that do not exist. */
|
|
39
|
+
export async function resolveDatabaseIds(client, org, server, refs, signal) {
|
|
40
|
+
const ids = [];
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const ref of refs) {
|
|
43
|
+
if (typeof ref === 'number' || /^\d+$/.test(ref)) {
|
|
44
|
+
ids.push(Number(ref));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const response = await client.get(databasesPath(org, server), {
|
|
48
|
+
query: { filter: { name: ref }, page: { size: 100 } },
|
|
49
|
+
signal,
|
|
50
|
+
});
|
|
51
|
+
const match = flattenCollection(response.data).items.find((database) => database.name === ref);
|
|
52
|
+
if (match)
|
|
53
|
+
ids.push(Number(match.id));
|
|
54
|
+
else
|
|
55
|
+
missing.push(ref);
|
|
56
|
+
}
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
throw new ToolInputError(`Database(s) not found on server ${server}: ${missing.join(', ')}. Check the names with forge_list_databases.`);
|
|
59
|
+
}
|
|
60
|
+
return ids;
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { operationOutput, queued } from '../shared/async.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT } from '../servers/shared.js';
|
|
5
|
+
import { databasesPath } from './shared.js';
|
|
6
|
+
export const syncDatabases = defineTool({
|
|
7
|
+
name: 'forge_sync_databases',
|
|
8
|
+
title: 'Synchronize databases',
|
|
9
|
+
description: 'Make Forge pick up databases created outside Forge (e.g. from a SQL client), so they can be managed and backed up.',
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.schemas.synchronizations.store'],
|
|
12
|
+
permissions: ['server:create-databases'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: false,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
},
|
|
22
|
+
outputSchema: operationOutput,
|
|
23
|
+
async handler(args, { client, organization, signal }) {
|
|
24
|
+
await client.post(`${databasesPath(organization(args.organization), args.server)}/synchronizations`, { signal });
|
|
25
|
+
// Which databases will appear is unknown in advance, so there is nothing to wait for.
|
|
26
|
+
return queued('synchronize the databases', 'forge_list_databases');
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define-tool.js';
|
|
3
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
4
|
+
import { SERVER_NOT_FOUND_HINT, serverPath } from '../servers/shared.js';
|
|
5
|
+
import { passwordInput } from './shared.js';
|
|
6
|
+
export const updateDatabaseRootPassword = defineTool({
|
|
7
|
+
name: 'forge_update_database_root_password',
|
|
8
|
+
title: 'Change database root password',
|
|
9
|
+
description: "Change the password of the database server's root user (the `forge` user on Forge-provisioned servers). Applications or tools using the old root password stop connecting.",
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.password.update'],
|
|
12
|
+
permissions: ['server:manage-services'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
notFoundHint: SERVER_NOT_FOUND_HINT,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
organization: organizationInput,
|
|
19
|
+
server: serverInput,
|
|
20
|
+
password: passwordInput.describe('New root password (never returned).'),
|
|
21
|
+
},
|
|
22
|
+
outputSchema: {
|
|
23
|
+
updated: z.boolean(),
|
|
24
|
+
},
|
|
25
|
+
async handler(args, { client, organization, signal }) {
|
|
26
|
+
await client.put(`${serverPath(organization(args.organization), args.server)}/database/password`, {
|
|
27
|
+
body: { password: args.password },
|
|
28
|
+
signal,
|
|
29
|
+
});
|
|
30
|
+
return { structured: { updated: true }, summary: 'Database root password changed.' };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineTool } from '../define-tool.js';
|
|
2
|
+
import { ToolInputError } from '../errors.js';
|
|
3
|
+
import { operationOutput, queued } from '../shared/async.js';
|
|
4
|
+
import { organizationInput, serverInput } from '../shared/schemas.js';
|
|
5
|
+
import { databaseRefsInput, databaseUserInput, databaseUsersPath, passwordInput, resolveDatabaseIds } from './shared.js';
|
|
6
|
+
export const updateDatabaseUser = defineTool({
|
|
7
|
+
name: 'forge_update_database_user',
|
|
8
|
+
title: 'Update database user',
|
|
9
|
+
description: "Change a database user's password and/or the databases it can access. `databases` replaces the whole list (by ID or name): include the ones it should keep.",
|
|
10
|
+
toolset: 'databases',
|
|
11
|
+
operations: ['organizations.servers.database.users.update', 'organizations.servers.database.schemas.index'],
|
|
12
|
+
permissions: ['server:create-databases', 'server:view'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
destructive: false,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
async: true,
|
|
17
|
+
notFoundHint: 'Check the user ID with forge_list_database_users.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
organization: organizationInput,
|
|
20
|
+
server: serverInput,
|
|
21
|
+
user: databaseUserInput,
|
|
22
|
+
password: passwordInput.optional().describe('New password (never returned).'),
|
|
23
|
+
databases: databaseRefsInput.optional().describe('Every database the user should access, by ID or name (replaces the list).'),
|
|
24
|
+
},
|
|
25
|
+
outputSchema: operationOutput,
|
|
26
|
+
async handler(args, { client, organization, signal }) {
|
|
27
|
+
if (args.password === undefined && args.databases === undefined) {
|
|
28
|
+
throw new ToolInputError('Pass `password` and/or `databases`.');
|
|
29
|
+
}
|
|
30
|
+
const org = organization(args.organization);
|
|
31
|
+
const databaseIds = args.databases ? await resolveDatabaseIds(client, org, args.server, args.databases, signal) : undefined;
|
|
32
|
+
await client.put(`${databaseUsersPath(org, args.server)}/${encodeURIComponent(String(args.user))}`, {
|
|
33
|
+
body: { password: args.password, database_ids: databaseIds },
|
|
34
|
+
signal,
|
|
35
|
+
});
|
|
36
|
+
// Neither passwords nor granted databases are exposed by the API: nothing to verify.
|
|
37
|
+
return queued(`update database user ${args.user}`, 'forge_list_database_users', '(the status shows "updating" while it runs)');
|
|
38
|
+
},
|
|
39
|
+
});
|
package/dist/tools/registry.js
CHANGED
|
@@ -3,6 +3,15 @@ import { deleteCertificate } from './certificates/delete-certificate.js';
|
|
|
3
3
|
import { getCertificate } from './certificates/get-certificate.js';
|
|
4
4
|
import { listCertificates } from './certificates/list-certificates.js';
|
|
5
5
|
import { runCertificateAction } from './certificates/run-certificate-action.js';
|
|
6
|
+
import { createDatabase } from './databases/create-database.js';
|
|
7
|
+
import { createDatabaseUser } from './databases/create-database-user.js';
|
|
8
|
+
import { deleteDatabase } from './databases/delete-database.js';
|
|
9
|
+
import { deleteDatabaseUser } from './databases/delete-database-user.js';
|
|
10
|
+
import { listDatabaseUsers } from './databases/list-database-users.js';
|
|
11
|
+
import { listDatabases } from './databases/list-databases.js';
|
|
12
|
+
import { syncDatabases } from './databases/sync-databases.js';
|
|
13
|
+
import { updateDatabaseRootPassword } from './databases/update-database-root-password.js';
|
|
14
|
+
import { updateDatabaseUser } from './databases/update-database-user.js';
|
|
6
15
|
import { createDeployKey } from './deployments/create-deploy-key.js';
|
|
7
16
|
import { createDeploymentWebhook } from './deployments/create-deployment-webhook.js';
|
|
8
17
|
import { deleteDeployKey } from './deployments/delete-deploy-key.js';
|
|
@@ -127,6 +136,16 @@ export const ALL_TOOLS = [
|
|
|
127
136
|
setPhpOpcache,
|
|
128
137
|
getPhpConfig,
|
|
129
138
|
updatePhpConfig,
|
|
139
|
+
// databases
|
|
140
|
+
listDatabases,
|
|
141
|
+
createDatabase,
|
|
142
|
+
deleteDatabase,
|
|
143
|
+
syncDatabases,
|
|
144
|
+
listDatabaseUsers,
|
|
145
|
+
createDatabaseUser,
|
|
146
|
+
updateDatabaseUser,
|
|
147
|
+
deleteDatabaseUser,
|
|
148
|
+
updateDatabaseRootPassword,
|
|
130
149
|
// deployments
|
|
131
150
|
listDeployments,
|
|
132
151
|
getDeployment,
|