@masonator/coolify-mcp 0.1.0 → 0.1.2
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 +129 -281
- package/dist/__tests__/coolify-client.test.js +242 -98
- package/dist/__tests__/mcp-server.test.js +143 -68
- package/dist/__tests__/resources/application-resources.test.d.ts +1 -0
- package/dist/__tests__/resources/application-resources.test.js +36 -0
- package/dist/__tests__/resources/database-resources.test.d.ts +1 -0
- package/dist/__tests__/resources/database-resources.test.js +72 -0
- package/dist/__tests__/resources/deployment-resources.test.d.ts +1 -0
- package/dist/__tests__/resources/deployment-resources.test.js +47 -0
- package/dist/__tests__/resources/service-resources.test.d.ts +1 -0
- package/dist/__tests__/resources/service-resources.test.js +81 -0
- package/dist/lib/coolify-client.d.ts +18 -6
- package/dist/lib/coolify-client.js +65 -15
- package/dist/lib/mcp-server.d.ts +24 -15
- package/dist/lib/mcp-server.js +270 -46
- package/dist/lib/resource.d.ts +13 -0
- package/dist/lib/resource.js +29 -0
- package/dist/resources/application-resources.d.ts +14 -0
- package/dist/resources/application-resources.js +59 -0
- package/dist/resources/database-resources.d.ts +17 -0
- package/dist/resources/database-resources.js +55 -0
- package/dist/resources/deployment-resources.d.ts +12 -0
- package/dist/resources/deployment-resources.js +48 -0
- package/dist/resources/index.d.ts +4 -0
- package/dist/resources/index.js +20 -0
- package/dist/resources/service-resources.d.ts +15 -0
- package/dist/resources/service-resources.js +55 -0
- package/dist/types/coolify.d.ts +163 -4
- package/package.json +8 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const database_resources_js_1 = require("../../resources/database-resources.js");
|
|
4
|
+
const coolify_client_js_1 = require("../../lib/coolify-client.js");
|
|
5
|
+
jest.mock('../../lib/coolify-client.js');
|
|
6
|
+
describe('DatabaseResources', () => {
|
|
7
|
+
let resources;
|
|
8
|
+
let mockClient;
|
|
9
|
+
const mockDatabase = {
|
|
10
|
+
id: 1,
|
|
11
|
+
uuid: 'test-db-uuid',
|
|
12
|
+
name: 'test-db',
|
|
13
|
+
description: 'Test database',
|
|
14
|
+
type: 'postgresql',
|
|
15
|
+
status: 'running',
|
|
16
|
+
created_at: '2024-03-06T12:00:00Z',
|
|
17
|
+
updated_at: '2024-03-06T12:00:00Z',
|
|
18
|
+
is_public: false,
|
|
19
|
+
image: 'postgres:latest',
|
|
20
|
+
postgres_user: 'postgres',
|
|
21
|
+
postgres_password: 'test123',
|
|
22
|
+
postgres_db: 'testdb',
|
|
23
|
+
};
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockClient = new coolify_client_js_1.CoolifyClient({
|
|
26
|
+
baseUrl: 'http://test.coolify.io',
|
|
27
|
+
accessToken: 'test-token',
|
|
28
|
+
});
|
|
29
|
+
resources = new database_resources_js_1.DatabaseResources(mockClient);
|
|
30
|
+
});
|
|
31
|
+
describe('listDatabases', () => {
|
|
32
|
+
it('should return a list of databases', async () => {
|
|
33
|
+
mockClient.listDatabases = jest.fn().mockResolvedValue([mockDatabase]);
|
|
34
|
+
const result = await resources.listDatabases();
|
|
35
|
+
expect(result).toEqual([mockDatabase]);
|
|
36
|
+
expect(mockClient.listDatabases).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('getDatabase', () => {
|
|
40
|
+
it('should return a specific database', async () => {
|
|
41
|
+
mockClient.getDatabase = jest.fn().mockResolvedValue(mockDatabase);
|
|
42
|
+
const result = await resources.getDatabase('test-db-uuid');
|
|
43
|
+
expect(result).toEqual(mockDatabase);
|
|
44
|
+
expect(mockClient.getDatabase).toHaveBeenCalledWith('test-db-uuid');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('updateDatabase', () => {
|
|
48
|
+
it('should update a database', async () => {
|
|
49
|
+
const updateData = {
|
|
50
|
+
name: 'updated-db',
|
|
51
|
+
description: 'Updated description',
|
|
52
|
+
};
|
|
53
|
+
mockClient.updateDatabase = jest.fn().mockResolvedValue({ ...mockDatabase, ...updateData });
|
|
54
|
+
const result = await resources.updateDatabase('test-db-uuid', updateData);
|
|
55
|
+
expect(result).toEqual({ ...mockDatabase, ...updateData });
|
|
56
|
+
expect(mockClient.updateDatabase).toHaveBeenCalledWith('test-db-uuid', updateData);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('deleteDatabase', () => {
|
|
60
|
+
it('should delete a database', async () => {
|
|
61
|
+
const mockResponse = { message: 'Database deleted' };
|
|
62
|
+
mockClient.deleteDatabase = jest.fn().mockResolvedValue(mockResponse);
|
|
63
|
+
const options = {
|
|
64
|
+
deleteConfigurations: true,
|
|
65
|
+
deleteVolumes: true,
|
|
66
|
+
};
|
|
67
|
+
const result = await resources.deleteDatabase('test-db-uuid', options);
|
|
68
|
+
expect(result).toEqual(mockResponse);
|
|
69
|
+
expect(mockClient.deleteDatabase).toHaveBeenCalledWith('test-db-uuid', options);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const deployment_resources_js_1 = require("../../resources/deployment-resources.js");
|
|
4
|
+
const coolify_client_js_1 = require("../../lib/coolify-client.js");
|
|
5
|
+
jest.mock('../../lib/coolify-client.js');
|
|
6
|
+
describe('DeploymentResources', () => {
|
|
7
|
+
let resources;
|
|
8
|
+
let mockClient;
|
|
9
|
+
const mockDeployment = {
|
|
10
|
+
id: 1,
|
|
11
|
+
uuid: 'test-deployment-uuid',
|
|
12
|
+
application_uuid: 'test-app-uuid',
|
|
13
|
+
status: 'running',
|
|
14
|
+
created_at: '2024-03-20T12:00:00Z',
|
|
15
|
+
updated_at: '2024-03-20T12:00:00Z',
|
|
16
|
+
};
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockClient = new coolify_client_js_1.CoolifyClient({
|
|
19
|
+
baseUrl: 'http://test.coolify.io',
|
|
20
|
+
accessToken: 'test-token',
|
|
21
|
+
});
|
|
22
|
+
resources = new deployment_resources_js_1.DeploymentResources(mockClient);
|
|
23
|
+
});
|
|
24
|
+
describe('listDeployments', () => {
|
|
25
|
+
it('should throw not implemented error', async () => {
|
|
26
|
+
await expect(resources.listDeployments()).rejects.toThrow('Not implemented');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('getDeployment', () => {
|
|
30
|
+
it('should throw not implemented error', async () => {
|
|
31
|
+
await expect(resources.getDeployment('test-id')).rejects.toThrow('Not implemented');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('deploy', () => {
|
|
35
|
+
it('should deploy an application', async () => {
|
|
36
|
+
mockClient.deployApplication = jest.fn().mockResolvedValue(mockDeployment);
|
|
37
|
+
const result = await resources.deploy({ uuid: 'test-app-uuid', forceRebuild: true });
|
|
38
|
+
expect(result).toEqual(mockDeployment);
|
|
39
|
+
expect(mockClient.deployApplication).toHaveBeenCalledWith('test-app-uuid');
|
|
40
|
+
});
|
|
41
|
+
it('should handle deployment errors', async () => {
|
|
42
|
+
const error = new Error('Failed to deploy application');
|
|
43
|
+
mockClient.deployApplication = jest.fn().mockRejectedValue(error);
|
|
44
|
+
await expect(resources.deploy({ uuid: 'test-app-uuid' })).rejects.toThrow('Failed to deploy application');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const service_resources_js_1 = require("../../resources/service-resources.js");
|
|
4
|
+
const coolify_client_js_1 = require("../../lib/coolify-client.js");
|
|
5
|
+
jest.mock('../../lib/coolify-client.js');
|
|
6
|
+
describe('ServiceResources', () => {
|
|
7
|
+
let resources;
|
|
8
|
+
let mockClient;
|
|
9
|
+
const mockService = {
|
|
10
|
+
id: 1,
|
|
11
|
+
uuid: 'test-service-uuid',
|
|
12
|
+
name: 'test-service',
|
|
13
|
+
description: 'Test service',
|
|
14
|
+
type: 'code-server',
|
|
15
|
+
status: 'running',
|
|
16
|
+
created_at: '2024-03-06T12:00:00Z',
|
|
17
|
+
updated_at: '2024-03-06T12:00:00Z',
|
|
18
|
+
project_uuid: 'test-project-uuid',
|
|
19
|
+
environment_name: 'production',
|
|
20
|
+
environment_uuid: 'test-env-uuid',
|
|
21
|
+
server_uuid: 'test-server-uuid',
|
|
22
|
+
domains: ['test-service.example.com'],
|
|
23
|
+
};
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockClient = new coolify_client_js_1.CoolifyClient({
|
|
26
|
+
baseUrl: 'http://test.coolify.io',
|
|
27
|
+
accessToken: 'test-token',
|
|
28
|
+
});
|
|
29
|
+
resources = new service_resources_js_1.ServiceResources(mockClient);
|
|
30
|
+
});
|
|
31
|
+
describe('listServices', () => {
|
|
32
|
+
it('should return a list of services', async () => {
|
|
33
|
+
mockClient.listServices = jest.fn().mockResolvedValue([mockService]);
|
|
34
|
+
const result = await resources.listServices();
|
|
35
|
+
expect(result).toEqual([mockService]);
|
|
36
|
+
expect(mockClient.listServices).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('getService', () => {
|
|
40
|
+
it('should return a specific service', async () => {
|
|
41
|
+
mockClient.getService = jest.fn().mockResolvedValue(mockService);
|
|
42
|
+
const result = await resources.getService('test-service-uuid');
|
|
43
|
+
expect(result).toEqual(mockService);
|
|
44
|
+
expect(mockClient.getService).toHaveBeenCalledWith('test-service-uuid');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('createService', () => {
|
|
48
|
+
it('should create a service', async () => {
|
|
49
|
+
const createData = {
|
|
50
|
+
type: 'code-server',
|
|
51
|
+
name: 'test-service',
|
|
52
|
+
description: 'Test service',
|
|
53
|
+
project_uuid: 'test-project-uuid',
|
|
54
|
+
environment_name: 'production',
|
|
55
|
+
server_uuid: 'test-server-uuid',
|
|
56
|
+
instant_deploy: true,
|
|
57
|
+
};
|
|
58
|
+
const mockResponse = {
|
|
59
|
+
uuid: 'test-service-uuid',
|
|
60
|
+
domains: ['test-service.example.com'],
|
|
61
|
+
};
|
|
62
|
+
mockClient.createService = jest.fn().mockResolvedValue(mockResponse);
|
|
63
|
+
const result = await resources.createService(createData);
|
|
64
|
+
expect(result).toEqual(mockResponse);
|
|
65
|
+
expect(mockClient.createService).toHaveBeenCalledWith(createData);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('deleteService', () => {
|
|
69
|
+
it('should delete a service', async () => {
|
|
70
|
+
const mockResponse = { message: 'Service deleted' };
|
|
71
|
+
mockClient.deleteService = jest.fn().mockResolvedValue(mockResponse);
|
|
72
|
+
const options = {
|
|
73
|
+
deleteConfigurations: true,
|
|
74
|
+
deleteVolumes: true,
|
|
75
|
+
};
|
|
76
|
+
const result = await resources.deleteService('test-service-uuid', options);
|
|
77
|
+
expect(result).toEqual(mockResponse);
|
|
78
|
+
expect(mockClient.deleteService).toHaveBeenCalledWith('test-service-uuid', options);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CoolifyConfig, ServerInfo, ServerResources, ServerDomain, ValidationResponse, Project, CreateProjectRequest, UpdateProjectRequest, Environment,
|
|
1
|
+
import { CoolifyConfig, ServerInfo, ServerResources, ServerDomain, ValidationResponse, Project, CreateProjectRequest, UpdateProjectRequest, Environment, Deployment, Database, DatabaseUpdateRequest, Service, CreateServiceRequest, DeleteServiceOptions } from '../types/coolify.js';
|
|
2
2
|
export declare class CoolifyClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private accessToken;
|
|
@@ -20,13 +20,25 @@ export declare class CoolifyClient {
|
|
|
20
20
|
message: string;
|
|
21
21
|
}>;
|
|
22
22
|
getProjectEnvironment(projectUuid: string, environmentNameOrUuid: string): Promise<Environment>;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
deployApplication(uuid: string): Promise<Deployment>;
|
|
24
|
+
listDatabases(): Promise<Database[]>;
|
|
25
|
+
getDatabase(uuid: string): Promise<Database>;
|
|
26
|
+
updateDatabase(uuid: string, data: DatabaseUpdateRequest): Promise<Database>;
|
|
27
|
+
deleteDatabase(uuid: string, options?: {
|
|
28
|
+
deleteConfigurations?: boolean;
|
|
29
|
+
deleteVolumes?: boolean;
|
|
30
|
+
dockerCleanup?: boolean;
|
|
31
|
+
deleteConnectedNetworks?: boolean;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
message: string;
|
|
34
|
+
}>;
|
|
35
|
+
listServices(): Promise<Service[]>;
|
|
36
|
+
getService(uuid: string): Promise<Service>;
|
|
37
|
+
createService(data: CreateServiceRequest): Promise<{
|
|
26
38
|
uuid: string;
|
|
39
|
+
domains: string[];
|
|
27
40
|
}>;
|
|
28
|
-
|
|
41
|
+
deleteService(uuid: string, options?: DeleteServiceOptions): Promise<{
|
|
29
42
|
message: string;
|
|
30
43
|
}>;
|
|
31
|
-
updateEnvironmentVariables(uuid: string, variables: UpdateEnvironmentVariablesRequest): Promise<Environment>;
|
|
32
44
|
}
|
|
@@ -85,28 +85,78 @@ class CoolifyClient {
|
|
|
85
85
|
async getProjectEnvironment(projectUuid, environmentNameOrUuid) {
|
|
86
86
|
return this.request(`/projects/${projectUuid}/${environmentNameOrUuid}`);
|
|
87
87
|
}
|
|
88
|
-
async
|
|
89
|
-
const
|
|
90
|
-
|
|
88
|
+
async deployApplication(uuid) {
|
|
89
|
+
const response = await this.request(`/applications/${uuid}/deploy`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
});
|
|
92
|
+
return response;
|
|
91
93
|
}
|
|
92
|
-
async
|
|
93
|
-
return this.request(
|
|
94
|
+
async listDatabases() {
|
|
95
|
+
return this.request('/databases');
|
|
94
96
|
}
|
|
95
|
-
async
|
|
96
|
-
return this.request(
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
async getDatabase(uuid) {
|
|
98
|
+
return this.request(`/databases/${uuid}`);
|
|
99
|
+
}
|
|
100
|
+
async updateDatabase(uuid, data) {
|
|
101
|
+
return this.request(`/databases/${uuid}`, {
|
|
102
|
+
method: 'PATCH',
|
|
103
|
+
body: JSON.stringify(data),
|
|
99
104
|
});
|
|
100
105
|
}
|
|
101
|
-
async
|
|
102
|
-
|
|
106
|
+
async deleteDatabase(uuid, options) {
|
|
107
|
+
const queryParams = new URLSearchParams();
|
|
108
|
+
if (options) {
|
|
109
|
+
if (options.deleteConfigurations !== undefined) {
|
|
110
|
+
queryParams.set('delete_configurations', options.deleteConfigurations.toString());
|
|
111
|
+
}
|
|
112
|
+
if (options.deleteVolumes !== undefined) {
|
|
113
|
+
queryParams.set('delete_volumes', options.deleteVolumes.toString());
|
|
114
|
+
}
|
|
115
|
+
if (options.dockerCleanup !== undefined) {
|
|
116
|
+
queryParams.set('docker_cleanup', options.dockerCleanup.toString());
|
|
117
|
+
}
|
|
118
|
+
if (options.deleteConnectedNetworks !== undefined) {
|
|
119
|
+
queryParams.set('delete_connected_networks', options.deleteConnectedNetworks.toString());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const queryString = queryParams.toString();
|
|
123
|
+
const url = queryString ? `/databases/${uuid}?${queryString}` : `/databases/${uuid}`;
|
|
124
|
+
return this.request(url, {
|
|
103
125
|
method: 'DELETE',
|
|
104
126
|
});
|
|
105
127
|
}
|
|
106
|
-
async
|
|
107
|
-
return this.request(
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
async listServices() {
|
|
129
|
+
return this.request('/services');
|
|
130
|
+
}
|
|
131
|
+
async getService(uuid) {
|
|
132
|
+
return this.request(`/services/${uuid}`);
|
|
133
|
+
}
|
|
134
|
+
async createService(data) {
|
|
135
|
+
return this.request('/services', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: JSON.stringify(data),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async deleteService(uuid, options) {
|
|
141
|
+
const queryParams = new URLSearchParams();
|
|
142
|
+
if (options) {
|
|
143
|
+
if (options.deleteConfigurations !== undefined) {
|
|
144
|
+
queryParams.set('delete_configurations', options.deleteConfigurations.toString());
|
|
145
|
+
}
|
|
146
|
+
if (options.deleteVolumes !== undefined) {
|
|
147
|
+
queryParams.set('delete_volumes', options.deleteVolumes.toString());
|
|
148
|
+
}
|
|
149
|
+
if (options.dockerCleanup !== undefined) {
|
|
150
|
+
queryParams.set('docker_cleanup', options.dockerCleanup.toString());
|
|
151
|
+
}
|
|
152
|
+
if (options.deleteConnectedNetworks !== undefined) {
|
|
153
|
+
queryParams.set('delete_connected_networks', options.deleteConnectedNetworks.toString());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const queryString = queryParams.toString();
|
|
157
|
+
const url = queryString ? `/services/${uuid}?${queryString}` : `/services/${uuid}`;
|
|
158
|
+
return this.request(url, {
|
|
159
|
+
method: 'DELETE',
|
|
110
160
|
});
|
|
111
161
|
}
|
|
112
162
|
}
|
package/dist/lib/mcp-server.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { Transport } from '@modelcontextprotocol/sdk/shared/transport
|
|
2
|
-
import { CoolifyConfig, ServerInfo, ServerResources, ServerDomain, ValidationResponse, Project, CreateProjectRequest, UpdateProjectRequest, Environment,
|
|
1
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport';
|
|
2
|
+
import { CoolifyConfig, ServerInfo, ServerResources, ServerDomain, ValidationResponse, Project, CreateProjectRequest, UpdateProjectRequest, Environment, Deployment, Database, DatabaseUpdateRequest, Service, CreateServiceRequest, DeleteServiceOptions } from '../types/coolify.js';
|
|
3
3
|
export declare class CoolifyMcpServer {
|
|
4
4
|
private server;
|
|
5
5
|
private client;
|
|
6
|
+
private databaseResources;
|
|
7
|
+
private deploymentResources;
|
|
8
|
+
private applicationResources;
|
|
9
|
+
private serviceResources;
|
|
6
10
|
constructor(config: CoolifyConfig);
|
|
7
11
|
private setupTools;
|
|
8
12
|
start(transport: Transport): Promise<void>;
|
|
@@ -21,22 +25,27 @@ export declare class CoolifyMcpServer {
|
|
|
21
25
|
message: string;
|
|
22
26
|
}>;
|
|
23
27
|
get_project_environment(projectUuid: string, environmentNameOrUuid: string): Promise<Environment>;
|
|
24
|
-
|
|
25
|
-
project_uuid?: string;
|
|
26
|
-
}): Promise<Environment[]>;
|
|
27
|
-
get_environment(params: {
|
|
28
|
-
uuid: string;
|
|
29
|
-
}): Promise<Environment>;
|
|
30
|
-
create_environment(params: CreateEnvironmentRequest): Promise<{
|
|
28
|
+
deploy_application(params: {
|
|
31
29
|
uuid: string;
|
|
30
|
+
}): Promise<Deployment>;
|
|
31
|
+
list_databases(): Promise<Database[]>;
|
|
32
|
+
get_database(uuid: string): Promise<Database>;
|
|
33
|
+
update_database(uuid: string, data: DatabaseUpdateRequest): Promise<Database>;
|
|
34
|
+
delete_database(uuid: string, options?: {
|
|
35
|
+
deleteConfigurations?: boolean;
|
|
36
|
+
deleteVolumes?: boolean;
|
|
37
|
+
dockerCleanup?: boolean;
|
|
38
|
+
deleteConnectedNetworks?: boolean;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
message: string;
|
|
32
41
|
}>;
|
|
33
|
-
|
|
42
|
+
list_services(): Promise<Service[]>;
|
|
43
|
+
get_service(uuid: string): Promise<Service>;
|
|
44
|
+
create_service(data: CreateServiceRequest): Promise<{
|
|
34
45
|
uuid: string;
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
uuid: string;
|
|
39
|
-
}): Promise<{
|
|
46
|
+
domains: string[];
|
|
47
|
+
}>;
|
|
48
|
+
delete_service(uuid: string, options?: DeleteServiceOptions): Promise<{
|
|
40
49
|
message: string;
|
|
41
50
|
}>;
|
|
42
51
|
}
|