aem-mcp-server 1.0.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 +7 -0
- package/.nvmrc +1 -0
- package/LICENSE +22 -0
- package/NOTICE.md +7 -0
- package/README.md +194 -0
- package/dist/aem/aem.config.js +49 -0
- package/dist/aem/aem.connector.js +858 -0
- package/dist/aem/aem.errors.js +133 -0
- package/dist/config.js +11 -0
- package/dist/explorer/api.explorer.js +25 -0
- package/dist/explorer/api.spec.js +79 -0
- package/dist/index.js +3 -0
- package/dist/mcp/mcp.aem-handler.js +152 -0
- package/dist/mcp/mcp.server-handler.js +71 -0
- package/dist/mcp/mcp.server.js +44 -0
- package/dist/mcp/mcp.tools.js +388 -0
- package/dist/mcp/mcp.transports.js +1 -0
- package/dist/server/app.auth.js +28 -0
- package/dist/server/app.server.js +82 -0
- package/package.json +51 -0
- package/src/aem/aem.config.ts +79 -0
- package/src/aem/aem.connector.ts +902 -0
- package/src/aem/aem.errors.ts +152 -0
- package/src/config.ts +12 -0
- package/src/explorer/api.explorer.ts +30 -0
- package/src/explorer/api.spec.ts +79 -0
- package/src/index.ts +4 -0
- package/src/mcp/mcp.aem-handler.ts +158 -0
- package/src/mcp/mcp.server-handler.ts +73 -0
- package/src/mcp/mcp.server.ts +51 -0
- package/src/mcp/mcp.tools.ts +397 -0
- package/src/mcp/mcp.transports.ts +5 -0
- package/src/server/app.auth.ts +32 -0
- package/src/server/app.server.ts +94 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export interface AEMErrorDetails {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class AEMOperationError extends Error {
|
|
6
|
+
code: string;
|
|
7
|
+
details?: AEMErrorDetails;
|
|
8
|
+
recoverable?: boolean;
|
|
9
|
+
retryAfter?: number;
|
|
10
|
+
|
|
11
|
+
constructor(error: {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
details?: AEMErrorDetails;
|
|
15
|
+
recoverable?: boolean;
|
|
16
|
+
retryAfter?: number;
|
|
17
|
+
}) {
|
|
18
|
+
super(error.message);
|
|
19
|
+
this.name = 'AEMOperationError';
|
|
20
|
+
this.code = error.code;
|
|
21
|
+
this.details = error.details;
|
|
22
|
+
this.recoverable = error.recoverable;
|
|
23
|
+
this.retryAfter = error.retryAfter;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AEM_ERROR_CODES = {
|
|
28
|
+
CONNECTION_FAILED: 'CONNECTION_FAILED',
|
|
29
|
+
TIMEOUT: 'TIMEOUT',
|
|
30
|
+
AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
|
|
31
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
32
|
+
INVALID_PATH: 'INVALID_PATH',
|
|
33
|
+
INVALID_COMPONENT_TYPE: 'INVALID_COMPONENT_TYPE',
|
|
34
|
+
INVALID_LOCALE: 'INVALID_LOCALE',
|
|
35
|
+
INVALID_PARAMETERS: 'INVALID_PARAMETERS',
|
|
36
|
+
RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
|
|
37
|
+
COMPONENT_NOT_FOUND: 'COMPONENT_NOT_FOUND',
|
|
38
|
+
PAGE_NOT_FOUND: 'PAGE_NOT_FOUND',
|
|
39
|
+
UPDATE_FAILED: 'UPDATE_FAILED',
|
|
40
|
+
VALIDATION_FAILED: 'VALIDATION_FAILED',
|
|
41
|
+
REPLICATION_FAILED: 'REPLICATION_FAILED',
|
|
42
|
+
QUERY_FAILED: 'QUERY_FAILED',
|
|
43
|
+
INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
|
|
44
|
+
SYSTEM_ERROR: 'SYSTEM_ERROR',
|
|
45
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export function createAEMError(
|
|
49
|
+
code: string,
|
|
50
|
+
message: string,
|
|
51
|
+
details?: AEMErrorDetails,
|
|
52
|
+
recoverable = false,
|
|
53
|
+
retryAfter?: number
|
|
54
|
+
): AEMOperationError {
|
|
55
|
+
return new AEMOperationError({ code, message, details, recoverable, retryAfter });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function handleAEMHttpError(error: any, operation: string): AEMOperationError {
|
|
59
|
+
if (error.response) {
|
|
60
|
+
const status = error.response.status;
|
|
61
|
+
const data = error.response.data;
|
|
62
|
+
switch (status) {
|
|
63
|
+
case 401:
|
|
64
|
+
return createAEMError(AEM_ERROR_CODES.AUTHENTICATION_FAILED, 'Authentication failed. Check AEM credentials.', { status, data });
|
|
65
|
+
case 403:
|
|
66
|
+
return createAEMError(AEM_ERROR_CODES.INSUFFICIENT_PERMISSIONS, 'Insufficient permissions for this operation.', { status, data, operation });
|
|
67
|
+
case 404:
|
|
68
|
+
return createAEMError(AEM_ERROR_CODES.RESOURCE_NOT_FOUND, 'Resource not found in AEM.', { status, data, operation });
|
|
69
|
+
case 429:
|
|
70
|
+
const retryAfter = error.response.headers['retry-after'];
|
|
71
|
+
return createAEMError(AEM_ERROR_CODES.RATE_LIMITED, 'Rate limit exceeded. Please try again later.', { status, data }, true, retryAfter ? parseInt(retryAfter) * 1000 : 60000);
|
|
72
|
+
case 500:
|
|
73
|
+
case 502:
|
|
74
|
+
case 503:
|
|
75
|
+
return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, 'AEM system error. Please try again later.', { status, data }, true, 30000);
|
|
76
|
+
default:
|
|
77
|
+
return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `HTTP ${status}: ${data?.message || 'Unknown error'}`, { status, data, operation });
|
|
78
|
+
}
|
|
79
|
+
} else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
80
|
+
return createAEMError(AEM_ERROR_CODES.CONNECTION_FAILED, 'Cannot connect to AEM instance. Check host and network.', { originalError: error.message }, true, 5000);
|
|
81
|
+
} else if (error.code === 'ETIMEDOUT') {
|
|
82
|
+
return createAEMError(AEM_ERROR_CODES.TIMEOUT, 'Request to AEM timed out.', { originalError: error.message }, true, 10000);
|
|
83
|
+
} else {
|
|
84
|
+
return createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, `Unexpected error during ${operation}: ${error.message}`, { originalError: error.message });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function safeExecute<T>(operation: () => Promise<T>, operationName: string, maxRetries = 3): Promise<T> {
|
|
89
|
+
let lastError: any;
|
|
90
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
91
|
+
try {
|
|
92
|
+
return await operation();
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
lastError = error instanceof AEMOperationError
|
|
95
|
+
? error
|
|
96
|
+
: handleAEMHttpError(error, operationName);
|
|
97
|
+
if (!lastError.recoverable || attempt === maxRetries) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const delay = lastError.retryAfter || Math.pow(2, attempt) * 1000;
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.warn(`[${operationName}] Attempt ${attempt} failed, retrying in ${delay}ms:`, lastError.message);
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw lastError;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function validateComponentOperation(locale: string, pagePath: string, component: string, props: any): void {
|
|
110
|
+
const errors: string[] = [];
|
|
111
|
+
if (!locale || typeof locale !== 'string') {
|
|
112
|
+
errors.push('Locale is required and must be a string');
|
|
113
|
+
}
|
|
114
|
+
if (!pagePath || typeof pagePath !== 'string') {
|
|
115
|
+
errors.push('Page path is required and must be a string');
|
|
116
|
+
} else if (!pagePath.startsWith('/content')) {
|
|
117
|
+
errors.push('Page path must start with /content');
|
|
118
|
+
}
|
|
119
|
+
if (!component || typeof component !== 'string') {
|
|
120
|
+
errors.push('Component type is required and must be a string');
|
|
121
|
+
}
|
|
122
|
+
if (!props || typeof props !== 'object') {
|
|
123
|
+
errors.push('Component properties are required and must be an object');
|
|
124
|
+
}
|
|
125
|
+
if (errors.length > 0) {
|
|
126
|
+
throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Invalid component operation parameters', { errors });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createSuccessResponse<T>(data: T, operation: string) {
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
operation,
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
data
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createErrorResponse(error: AEMOperationError, operation: string) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
operation,
|
|
143
|
+
timestamp: new Date().toISOString(),
|
|
144
|
+
error: {
|
|
145
|
+
code: error.code,
|
|
146
|
+
message: error.message,
|
|
147
|
+
details: error.details,
|
|
148
|
+
recoverable: error.recoverable,
|
|
149
|
+
retryAfter: error.retryAfter
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const SERVER_PORT = parseInt(process.env.SERVER_PORT || '3000', 10);
|
|
2
|
+
const MCP_PORT = parseInt(process.env.MCP_PORT || '8080', 10);
|
|
3
|
+
const MCP_USERNAME = process.env.MCP_USERNAME || 'admin';
|
|
4
|
+
const MCP_PASSWORD = process.env.MCP_PASSWORD || 'admin';
|
|
5
|
+
const APP_VERSION = process.env.npm_package_version || '1.0.0';
|
|
6
|
+
|
|
7
|
+
export const config = {
|
|
8
|
+
SERVER_PORT,
|
|
9
|
+
MCP_USERNAME,
|
|
10
|
+
MCP_PASSWORD,
|
|
11
|
+
APP_VERSION
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import swaggerUi from 'swagger-ui-express';
|
|
2
|
+
import swaggerJSDoc from 'swagger-jsdoc';
|
|
3
|
+
import { Express, Request, Response } from 'express';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { apiPaths } from './api.spec.js';
|
|
6
|
+
|
|
7
|
+
const swaggerDefinition = {
|
|
8
|
+
openapi: '3.0.0',
|
|
9
|
+
info: {
|
|
10
|
+
title: 'AEM MCP Gateway API',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
description: 'API documentation for the AEM MCP Gateway Server',
|
|
13
|
+
},
|
|
14
|
+
servers: [
|
|
15
|
+
{ url: `http://localhost:${config.SERVER_PORT}` },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const options = {
|
|
20
|
+
swaggerDefinition,
|
|
21
|
+
apis: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const openapiSpec: any = swaggerJSDoc(options);
|
|
25
|
+
openapiSpec.paths = apiPaths;
|
|
26
|
+
|
|
27
|
+
export const useExplorer = (app: Express) => {
|
|
28
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec));
|
|
29
|
+
app.get('/openapi.json', (req: Request, res: Response) => { res.json(openapiSpec); });
|
|
30
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const apiPaths = {
|
|
2
|
+
'/mcp': {
|
|
3
|
+
post: {
|
|
4
|
+
summary: 'JSON-RPC endpoint for MCP calls',
|
|
5
|
+
description: 'Call MCP methods using JSON-RPC 2.0. The method and params must be provided in the request body.',
|
|
6
|
+
requestBody: {
|
|
7
|
+
required: true,
|
|
8
|
+
content: {
|
|
9
|
+
'application/json': {
|
|
10
|
+
schema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
jsonrpc: { type: 'string', example: '2.0' },
|
|
14
|
+
id: { type: 'integer', example: 1 },
|
|
15
|
+
method: { type: 'string', example: 'listMethods' },
|
|
16
|
+
params: { type: 'object' },
|
|
17
|
+
},
|
|
18
|
+
required: ['jsonrpc', 'id', 'method'],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
responses: {
|
|
24
|
+
200: {
|
|
25
|
+
description: 'JSON-RPC response',
|
|
26
|
+
content: {
|
|
27
|
+
'application/json': {
|
|
28
|
+
schema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
jsonrpc: { type: 'string', example: '2.0' },
|
|
32
|
+
id: { type: 'integer', example: 1 },
|
|
33
|
+
result: { type: 'object' },
|
|
34
|
+
error: { type: 'object' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
'/mcp/methods': {
|
|
44
|
+
get: {
|
|
45
|
+
summary: 'List all available MCP methods',
|
|
46
|
+
description: 'Returns a list of all available MCP methods and their parameters.',
|
|
47
|
+
responses: {
|
|
48
|
+
200: {
|
|
49
|
+
description: 'A list of MCP methods',
|
|
50
|
+
content: {
|
|
51
|
+
'application/json': {
|
|
52
|
+
schema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
methods: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
name: { type: 'string' },
|
|
61
|
+
description: { type: 'string' },
|
|
62
|
+
parameters: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
total: { type: 'integer' },
|
|
70
|
+
timestamp: { type: 'string', format: 'date-time' },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { AEMConnector } from '../aem/aem.connector.js';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
|
|
4
|
+
export class MCPRequestHandler {
|
|
5
|
+
aemConnector: AEMConnector;
|
|
6
|
+
|
|
7
|
+
constructor(aemConnector: AEMConnector) {
|
|
8
|
+
this.aemConnector = aemConnector;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async handleRequest(method: string, params: any) {
|
|
12
|
+
try {
|
|
13
|
+
switch (method) {
|
|
14
|
+
case 'validateComponent':
|
|
15
|
+
return await this.aemConnector.validateComponent(params);
|
|
16
|
+
case 'updateComponent':
|
|
17
|
+
return await this.aemConnector.updateComponent(params);
|
|
18
|
+
case 'undoChanges':
|
|
19
|
+
return await this.aemConnector.undoChanges(params);
|
|
20
|
+
case 'scanPageComponents':
|
|
21
|
+
return await this.aemConnector.scanPageComponents(params.pagePath);
|
|
22
|
+
case 'fetchSites':
|
|
23
|
+
return await this.aemConnector.fetchSites();
|
|
24
|
+
case 'fetchLanguageMasters':
|
|
25
|
+
return await this.aemConnector.fetchLanguageMasters(params.site);
|
|
26
|
+
case 'fetchAvailableLocales':
|
|
27
|
+
return await this.aemConnector.fetchAvailableLocales(params.site, params.languageMasterPath);
|
|
28
|
+
case 'replicateAndPublish':
|
|
29
|
+
return await this.aemConnector.replicateAndPublish(params.selectedLocales, params.componentData, params.localizedOverrides);
|
|
30
|
+
case 'getAllTextContent':
|
|
31
|
+
return await this.aemConnector.getAllTextContent(params.pagePath);
|
|
32
|
+
case 'getPageTextContent':
|
|
33
|
+
return await this.aemConnector.getPageTextContent(params.pagePath);
|
|
34
|
+
case 'getPageImages':
|
|
35
|
+
return await this.aemConnector.getPageImages(params.pagePath);
|
|
36
|
+
case 'updateImagePath':
|
|
37
|
+
return await this.aemConnector.updateImagePath(params.componentPath, params.newImagePath);
|
|
38
|
+
case 'getPageContent':
|
|
39
|
+
return await this.aemConnector.getPageContent(params.pagePath);
|
|
40
|
+
case 'listPages':
|
|
41
|
+
return await this.aemConnector.listPages(params.siteRoot || params.path || '/content', params.depth || 1, params.limit || 20);
|
|
42
|
+
case 'getNodeContent':
|
|
43
|
+
return await this.aemConnector.getNodeContent(params.path, params.depth || 1);
|
|
44
|
+
case 'listChildren':
|
|
45
|
+
return await this.aemConnector.listChildren(params.path);
|
|
46
|
+
case 'getPageProperties':
|
|
47
|
+
return await this.aemConnector.getPageProperties(params.pagePath);
|
|
48
|
+
case 'searchContent':
|
|
49
|
+
return await this.aemConnector.searchContent(params);
|
|
50
|
+
case 'executeJCRQuery':
|
|
51
|
+
return await this.aemConnector.executeJCRQuery(params.query, params.limit);
|
|
52
|
+
case 'getAssetMetadata':
|
|
53
|
+
return await this.aemConnector.getAssetMetadata(params.assetPath);
|
|
54
|
+
case 'getStatus':
|
|
55
|
+
return this.getWorkflowStatus(params.workflowId);
|
|
56
|
+
case 'listMethods':
|
|
57
|
+
return { methods: this.getAvailableMethods() };
|
|
58
|
+
case 'enhancedPageSearch':
|
|
59
|
+
return await this.aemConnector.searchContent({
|
|
60
|
+
fulltext: params.searchTerm,
|
|
61
|
+
path: params.basePath,
|
|
62
|
+
type: 'cq:Page',
|
|
63
|
+
limit: 20
|
|
64
|
+
});
|
|
65
|
+
case 'createPage':
|
|
66
|
+
return await this.aemConnector.createPage(params);
|
|
67
|
+
case 'deletePage':
|
|
68
|
+
return await this.aemConnector.deletePage(params);
|
|
69
|
+
case 'createComponent':
|
|
70
|
+
return await this.aemConnector.createComponent(params);
|
|
71
|
+
case 'deleteComponent':
|
|
72
|
+
return await this.aemConnector.deleteComponent(params);
|
|
73
|
+
case 'unpublishContent':
|
|
74
|
+
return await this.aemConnector.unpublishContent(params);
|
|
75
|
+
case 'activatePage':
|
|
76
|
+
return await this.aemConnector.activatePage(params);
|
|
77
|
+
case 'deactivatePage':
|
|
78
|
+
return await this.aemConnector.deactivatePage(params);
|
|
79
|
+
case 'uploadAsset':
|
|
80
|
+
return await this.aemConnector.uploadAsset(params);
|
|
81
|
+
case 'updateAsset':
|
|
82
|
+
return await this.aemConnector.updateAsset(params);
|
|
83
|
+
case 'deleteAsset':
|
|
84
|
+
return await this.aemConnector.deleteAsset(params);
|
|
85
|
+
case 'getTemplates':
|
|
86
|
+
return await this.aemConnector.getTemplates(params.sitePath);
|
|
87
|
+
case 'getTemplateStructure':
|
|
88
|
+
return await this.aemConnector.getTemplateStructure(params.templatePath);
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`Unknown method: ${method}`);
|
|
91
|
+
}
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
return { error: error.message, method, params };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getWorkflowStatus(workflowId: string) {
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
workflowId: workflowId,
|
|
101
|
+
status: 'completed',
|
|
102
|
+
message: 'Mock workflow status - always returns completed',
|
|
103
|
+
timestamp: new Date().toISOString()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getAvailableMethods() {
|
|
108
|
+
return [
|
|
109
|
+
{ name: 'validateComponent', description: 'Validate component changes before applying them', parameters: ['locale', 'page_path', 'component', 'props'] },
|
|
110
|
+
{ name: 'updateComponent', description: 'Update component properties in AEM', parameters: ['componentPath', 'properties'] },
|
|
111
|
+
{ name: 'undoChanges', description: 'Undo the last component changes', parameters: ['job_id'] },
|
|
112
|
+
{ name: 'scanPageComponents', description: 'Scan a page to discover all components and their properties', parameters: ['pagePath'] },
|
|
113
|
+
{ name: 'fetchSites', description: 'Get all available sites in AEM', parameters: [] },
|
|
114
|
+
{ name: 'fetchLanguageMasters', description: 'Get language masters for a specific site', parameters: ['site'] },
|
|
115
|
+
{ name: 'fetchAvailableLocales', description: 'Get available locales for a site and language master', parameters: ['site', 'languageMasterPath'] },
|
|
116
|
+
{ name: 'replicateAndPublish', description: 'Replicate and publish content to selected locales', parameters: ['selectedLocales', 'componentData', 'localizedOverrides'] },
|
|
117
|
+
{ name: 'getAllTextContent', description: 'Get all text content from a page including titles, text components, and descriptions', parameters: ['pagePath'] },
|
|
118
|
+
{ name: 'getPageTextContent', description: 'Get text content from a specific page', parameters: ['pagePath'] },
|
|
119
|
+
{ name: 'getPageImages', description: 'Get all images from a page, including those within Experience Fragments', parameters: ['pagePath'] },
|
|
120
|
+
{ name: 'updateImagePath', description: 'Update the image path for an image component and verify the update', parameters: ['componentPath', 'newImagePath'] },
|
|
121
|
+
{ name: 'getPageContent', description: 'Get all content from a page including Experience Fragments and Content Fragments', parameters: ['pagePath'] },
|
|
122
|
+
{ name: 'listPages', description: 'List all pages under a site root', parameters: ['siteRoot', 'depth', 'limit'] },
|
|
123
|
+
{ name: 'getNodeContent', description: 'Legacy: Get JCR node content', parameters: ['path', 'depth'] },
|
|
124
|
+
{ name: 'listChildren', description: 'Legacy: List child nodes', parameters: ['path'] },
|
|
125
|
+
{ name: 'getPageProperties', description: 'Get page properties', parameters: ['pagePath'] },
|
|
126
|
+
{ name: 'searchContent', description: 'Search content using Query Builder', parameters: ['type', 'fulltext', 'path', 'limit'] },
|
|
127
|
+
{ name: 'executeJCRQuery', description: 'Execute JCR query', parameters: ['query', 'limit'] },
|
|
128
|
+
{ name: 'getAssetMetadata', description: 'Get asset metadata', parameters: ['assetPath'] },
|
|
129
|
+
{ name: 'getStatus', description: 'Get workflow status by ID', parameters: ['workflowId'] },
|
|
130
|
+
{ name: 'listMethods', description: 'Get list of available MCP methods', parameters: [] },
|
|
131
|
+
{ name: 'enhancedPageSearch', description: 'Intelligent page search with comprehensive fallback strategies and cross-section search', parameters: ['searchTerm', 'basePath', 'includeAlternateLocales'] },
|
|
132
|
+
{ name: 'createPage', description: 'Create a new page in AEM', parameters: ['parentPath', 'title', 'template', 'name', 'properties'] },
|
|
133
|
+
{ name: 'deletePage', description: 'Delete a page from AEM', parameters: ['pagePath', 'force'] },
|
|
134
|
+
{ name: 'createComponent', description: 'Create a new component on a page', parameters: ['pagePath', 'componentType', 'resourceType', 'properties', 'name'] },
|
|
135
|
+
{ name: 'deleteComponent', description: 'Delete a component from AEM', parameters: ['componentPath', 'force'] },
|
|
136
|
+
{ name: 'unpublishContent', description: 'Unpublish content from the publish environment', parameters: ['contentPaths', 'unpublishTree'] },
|
|
137
|
+
{ name: 'activatePage', description: 'Activate (publish) a single page', parameters: ['pagePath', 'activateTree'] },
|
|
138
|
+
{ name: 'deactivatePage', description: 'Deactivate (unpublish) a single page', parameters: ['pagePath', 'deactivateTree'] },
|
|
139
|
+
{ name: 'uploadAsset', description: 'Upload a new asset to AEM DAM', parameters: ['parentPath', 'fileName', 'fileContent', 'mimeType', 'metadata'] },
|
|
140
|
+
{ name: 'updateAsset', description: 'Update an existing asset in AEM DAM', parameters: ['assetPath', 'metadata', 'fileContent', 'mimeType'] },
|
|
141
|
+
{ name: 'deleteAsset', description: 'Delete an asset from AEM DAM', parameters: ['assetPath', 'force'] },
|
|
142
|
+
{ name: 'getTemplates', description: 'Get available page templates', parameters: ['sitePath'] },
|
|
143
|
+
{ name: 'getTemplateStructure', description: 'Get detailed structure of a specific template', parameters: ['templatePath'] },
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async handleHealthCheck() {
|
|
148
|
+
const aemConnected = await this.aemConnector.testConnection();
|
|
149
|
+
return {
|
|
150
|
+
status: 'healthy',
|
|
151
|
+
aem: aemConnected ? 'connected' : 'disconnected',
|
|
152
|
+
mcp: 'ready',
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
version: config.APP_VERSION || '1.0.0',
|
|
155
|
+
port: config.SERVER_PORT,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { transports } from './mcp.transports.js';
|
|
6
|
+
import { createMCPServer } from './mcp.server.js';
|
|
7
|
+
|
|
8
|
+
export const handleRequest = async (req: Request, res: Response) => {
|
|
9
|
+
console.log('Received MCP request:', req.body);
|
|
10
|
+
const { jsonrpc, id, method, params } = req.body;
|
|
11
|
+
if (jsonrpc !== '2.0' || !method) {
|
|
12
|
+
res.status(400).json({
|
|
13
|
+
jsonrpc: '2.0',
|
|
14
|
+
id: id || null,
|
|
15
|
+
error: { code: -32600, message: 'Invalid Request', data: 'Must be valid JSON-RPC 2.0' },
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// Check for existing session ID
|
|
21
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
22
|
+
let transport: StreamableHTTPServerTransport;
|
|
23
|
+
|
|
24
|
+
if (sessionId && transports[sessionId]) {
|
|
25
|
+
// Reuse existing transport
|
|
26
|
+
transport = transports[sessionId]
|
|
27
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
28
|
+
// New initialization request - use JSON response mode
|
|
29
|
+
transport = new StreamableHTTPServerTransport({
|
|
30
|
+
sessionIdGenerator: () => randomUUID(),
|
|
31
|
+
enableJsonResponse: true, // Enable JSON response mode
|
|
32
|
+
onsessioninitialized: (sessionId) => {
|
|
33
|
+
// Store the transport by session ID when session is initialized
|
|
34
|
+
// This avoids race conditions where requests might come in before the session is stored
|
|
35
|
+
console.log(`Session initialized with ID: ${sessionId}`);
|
|
36
|
+
transports[sessionId] = transport;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Connect the transport to the MCP server BEFORE handling the request
|
|
41
|
+
const server = createMCPServer();
|
|
42
|
+
await server.connect(transport);
|
|
43
|
+
await transport.handleRequest(req, res, req.body);
|
|
44
|
+
return; // Already handled
|
|
45
|
+
} else {
|
|
46
|
+
// Invalid request - no session ID or not initialization request
|
|
47
|
+
res.status(400).json({
|
|
48
|
+
jsonrpc: '2.0',
|
|
49
|
+
error: {
|
|
50
|
+
code: -32000,
|
|
51
|
+
message: 'Bad Request: No valid session ID provided',
|
|
52
|
+
},
|
|
53
|
+
id: null,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle the request with existing transport - no need to reconnect
|
|
59
|
+
await transport.handleRequest(req, res, req.body);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Error handling MCP request:', error);
|
|
62
|
+
if (!res.headersSent) {
|
|
63
|
+
res.status(500).json({
|
|
64
|
+
jsonrpc: '2.0',
|
|
65
|
+
error: {
|
|
66
|
+
code: -32603,
|
|
67
|
+
message: 'Internal server error',
|
|
68
|
+
},
|
|
69
|
+
id: null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CallToolResult, CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { tools } from './mcp.tools.js';
|
|
6
|
+
import { AEMConnector } from '../aem/aem.connector.js';
|
|
7
|
+
import { MCPRequestHandler } from './mcp.aem-handler.js';
|
|
8
|
+
|
|
9
|
+
export const createMCPServer = () => {
|
|
10
|
+
const aemConnector = new AEMConnector();
|
|
11
|
+
const mcpHandler = new MCPRequestHandler(aemConnector);
|
|
12
|
+
|
|
13
|
+
const server = new Server({
|
|
14
|
+
name: 'aem-mcp-server',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
}, {
|
|
17
|
+
capabilities: {
|
|
18
|
+
resources: {},
|
|
19
|
+
tools: {},
|
|
20
|
+
prompts: {},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
25
|
+
return { tools };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
29
|
+
const { name, arguments: args } = request.params;
|
|
30
|
+
|
|
31
|
+
if (!args) {
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{ type: 'text', text: 'Error: No arguments provided' },
|
|
35
|
+
],
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const result = await mcpHandler.handleRequest(name, args);
|
|
41
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return server;
|
|
51
|
+
}
|