@webiny/mcp 6.2.0 → 6.3.0-beta.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/package.json +4 -4
- package/skills/admin/admin-architect/SKILL.md +84 -81
- package/skills/api/event-handler-pattern/SKILL.md +56 -60
- package/skills/api/http-route/SKILL.md +0 -1
- package/skills/api/use-case-pattern/SKILL.md +137 -124
- package/skills/dependency-injection/SKILL.md +78 -79
- package/skills/generated/admin/SKILL.md +16 -1
- package/skills/generated/api/SKILL.md +43 -1
- package/skills/webiny-sdk/SKILL.md +148 -29
- package/skills/website-builder/SKILL.md +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webiny/mcp",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/lodash": "4.17.24",
|
|
27
27
|
"@types/ncp": "2.0.8",
|
|
28
|
-
"@webiny/build-tools": "6.
|
|
28
|
+
"@webiny/build-tools": "6.3.0-beta.0",
|
|
29
29
|
"execa": "5.1.1",
|
|
30
30
|
"tsx": "4.21.0",
|
|
31
|
-
"typescript": "
|
|
31
|
+
"typescript": "6.0.3"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"prepublishOnly": "bash ./prepublishOnly.sh"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "94c21e58aebc9855bf1ae972423281faa0f5c135"
|
|
37
37
|
}
|
|
@@ -317,27 +317,27 @@ import { makeAutoObservable, runInAction } from "mobx";
|
|
|
317
317
|
import { WcpService as ServiceAbstraction, WcpGateway } from "./abstractions.js";
|
|
318
318
|
|
|
319
319
|
class WcpServiceImpl implements ServiceAbstraction.Interface {
|
|
320
|
-
|
|
320
|
+
private project: ILicense | null = null;
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
constructor(private gateway: WcpGateway.Interface) {
|
|
323
|
+
makeAutoObservable(this);
|
|
324
|
+
}
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
getProject(): ILicense {
|
|
327
|
+
return this.project;
|
|
328
|
+
}
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
330
|
+
async loadProject(): Promise<void> {
|
|
331
|
+
const data = await this.gateway.fetchProject();
|
|
332
|
+
runInAction(() => {
|
|
333
|
+
this.project = data;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
export const WcpService = ServiceAbstraction.createImplementation({
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
implementation: WcpServiceImpl,
|
|
340
|
+
dependencies: [WcpGateway]
|
|
341
341
|
});
|
|
342
342
|
```
|
|
343
343
|
|
|
@@ -352,37 +352,37 @@ Repositories own domain data and cache. They use MobX for reactivity:
|
|
|
352
352
|
```ts
|
|
353
353
|
import { makeAutoObservable, runInAction } from "mobx";
|
|
354
354
|
import {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
NextjsConfigRepository as RepositoryAbstraction,
|
|
356
|
+
NextjsConfigGateway,
|
|
357
|
+
NextjsConfig
|
|
358
358
|
} from "./abstractions.js";
|
|
359
359
|
|
|
360
360
|
class NextjsConfigRepositoryImpl implements RepositoryAbstraction.Interface {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
constructor(private gateway: NextjsConfigGateway.Interface) {
|
|
364
|
-
makeAutoObservable(this);
|
|
365
|
-
}
|
|
361
|
+
private config: NextjsConfig | undefined = undefined;
|
|
366
362
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
363
|
+
constructor(private gateway: NextjsConfigGateway.Interface) {
|
|
364
|
+
makeAutoObservable(this);
|
|
365
|
+
}
|
|
370
366
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
367
|
+
getConfig(): NextjsConfig | undefined {
|
|
368
|
+
return this.config;
|
|
369
|
+
}
|
|
375
370
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
371
|
+
async loadConfig(): Promise<void> {
|
|
372
|
+
if (this.config) {
|
|
373
|
+
return; // Already loaded — cache hit
|
|
380
374
|
}
|
|
375
|
+
|
|
376
|
+
const config = await this.gateway.getConfig();
|
|
377
|
+
runInAction(() => {
|
|
378
|
+
this.config = config;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
export const NextjsConfigRepository = RepositoryAbstraction.createImplementation({
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
implementation: NextjsConfigRepositoryImpl,
|
|
385
|
+
dependencies: [NextjsConfigGateway]
|
|
386
386
|
});
|
|
387
387
|
```
|
|
388
388
|
|
|
@@ -395,48 +395,53 @@ import { NextjsConfigGateway as GatewayAbstraction } from "./abstractions.js";
|
|
|
395
395
|
import { GraphQLClient } from "@webiny/app/features/graphqlClient";
|
|
396
396
|
|
|
397
397
|
const GET_NEXTJS_CONFIG = /* GraphQL */ `
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
query GetNextjsConfig {
|
|
399
|
+
websiteBuilder {
|
|
400
|
+
getNextjsConfig {
|
|
401
|
+
data
|
|
402
|
+
error {
|
|
403
|
+
code
|
|
404
|
+
message
|
|
405
|
+
data
|
|
404
406
|
}
|
|
407
|
+
}
|
|
405
408
|
}
|
|
409
|
+
}
|
|
406
410
|
`;
|
|
407
411
|
|
|
408
412
|
type GetNextjsConfigResponse = {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
websiteBuilder: {
|
|
414
|
+
getNextjsConfig:
|
|
415
|
+
| { data: string; error: null }
|
|
416
|
+
| { data: null; error: { code: string; message: string; data: any } };
|
|
417
|
+
};
|
|
414
418
|
};
|
|
415
419
|
|
|
416
420
|
class NextjsGraphQLGateway implements GatewayAbstraction.Interface {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
async getConfig(): Promise<string> {
|
|
420
|
-
const response = await this.client.execute<GetNextjsConfigResponse>({
|
|
421
|
-
query: GET_NEXTJS_CONFIG
|
|
422
|
-
});
|
|
421
|
+
constructor(private client: GraphQLClient.Interface) {}
|
|
423
422
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
423
|
+
async getConfig(): Promise<string> {
|
|
424
|
+
const response = await this.client.execute<GetNextjsConfigResponse>({
|
|
425
|
+
query: GET_NEXTJS_CONFIG
|
|
426
|
+
});
|
|
428
427
|
|
|
429
|
-
|
|
428
|
+
const envelope = response.websiteBuilder.getNextjsConfig;
|
|
429
|
+
if (envelope.error) {
|
|
430
|
+
throw new Error(envelope.error.message);
|
|
430
431
|
}
|
|
432
|
+
|
|
433
|
+
return envelope.data;
|
|
434
|
+
}
|
|
431
435
|
}
|
|
432
436
|
|
|
433
437
|
export const NextjsConfigGateway = GatewayAbstraction.createImplementation({
|
|
434
|
-
|
|
435
|
-
|
|
438
|
+
implementation: NextjsGraphQLGateway,
|
|
439
|
+
dependencies: [GraphQLClient]
|
|
436
440
|
});
|
|
437
441
|
```
|
|
438
442
|
|
|
439
443
|
**Key points:**
|
|
444
|
+
|
|
440
445
|
- Define the GraphQL query as a string constant with `/* GraphQL */` comment for syntax highlighting
|
|
441
446
|
- Type the response shape explicitly
|
|
442
447
|
- Handle the `data`/`error` envelope pattern
|
|
@@ -450,12 +455,12 @@ When grouping related features, create a composite with no `resolve`:
|
|
|
450
455
|
import { createFeature } from "webiny/admin";
|
|
451
456
|
|
|
452
457
|
export const FoldersFeature = createFeature({
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
name: "Folders",
|
|
459
|
+
register(container) {
|
|
460
|
+
CreateFolderFeature.register(container);
|
|
461
|
+
UpdateFolderFeature.register(container);
|
|
462
|
+
DeleteFolderFeature.register(container);
|
|
463
|
+
}
|
|
459
464
|
});
|
|
460
465
|
```
|
|
461
466
|
|
|
@@ -465,17 +470,14 @@ Decorators add cross-cutting concerns without modifying the core implementation:
|
|
|
465
470
|
|
|
466
471
|
```ts
|
|
467
472
|
class ListFoldersUseCaseWithLoading implements UseCaseAbstraction.Interface {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
LoadingActionsEnum.list
|
|
477
|
-
);
|
|
478
|
-
}
|
|
473
|
+
constructor(
|
|
474
|
+
private loadingRepository: FoldersLoadingRepository.Interface,
|
|
475
|
+
private decoratee: UseCaseAbstraction.Interface // decoratee is LAST
|
|
476
|
+
) {}
|
|
477
|
+
|
|
478
|
+
async execute() {
|
|
479
|
+
await this.loadingRepository.runCallBack(this.decoratee.execute(), LoadingActionsEnum.list);
|
|
480
|
+
}
|
|
479
481
|
}
|
|
480
482
|
```
|
|
481
483
|
|
|
@@ -483,14 +485,15 @@ Register with `container.registerDecorator()`:
|
|
|
483
485
|
|
|
484
486
|
```ts
|
|
485
487
|
export const MyExtensionFeature = createFeature({
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
488
|
+
name: "MyExtension",
|
|
489
|
+
register(container) {
|
|
490
|
+
container.registerDecorator(MyPresenterDecorator);
|
|
491
|
+
}
|
|
490
492
|
});
|
|
491
493
|
```
|
|
492
494
|
|
|
493
495
|
**Rules:**
|
|
496
|
+
|
|
494
497
|
- Implements the same interface as the decorated abstraction
|
|
495
498
|
- Constructor: extra dependencies first, `decoratee` **last**
|
|
496
499
|
- The `dependencies` array does NOT include the decoratee
|
|
@@ -90,7 +90,9 @@ class MyHandler implements SomeEventHandler.Interface {
|
|
|
90
90
|
constructor(private someUseCase: SomeUseCase.Interface) {}
|
|
91
91
|
|
|
92
92
|
async handle(event: SomeEventHandler.Event) {
|
|
93
|
-
const result = await this.someUseCase.execute({
|
|
93
|
+
const result = await this.someUseCase.execute({
|
|
94
|
+
/* ... */
|
|
95
|
+
});
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -120,30 +122,30 @@ import type { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events
|
|
|
120
122
|
|
|
121
123
|
// Event Payload Types
|
|
122
124
|
export interface EntityBeforeDisablePayload {
|
|
123
|
-
|
|
125
|
+
entity: Entity;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
export interface EntityAfterDisablePayload {
|
|
127
|
-
|
|
129
|
+
entity: Entity;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
// Handler Abstractions — one per event
|
|
131
133
|
export const EntityBeforeDisableEventHandler = createAbstraction<
|
|
132
|
-
|
|
134
|
+
IEventHandler<EntityBeforeDisableEvent>
|
|
133
135
|
>("MyPackage/EntityBeforeDisableEventHandler");
|
|
134
136
|
|
|
135
137
|
export namespace EntityBeforeDisableEventHandler {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
export type Interface = IEventHandler<EntityBeforeDisableEvent>;
|
|
139
|
+
export type Event = EntityBeforeDisableEvent;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
export const EntityAfterDisableEventHandler = createAbstraction<
|
|
141
|
-
|
|
143
|
+
IEventHandler<EntityAfterDisableEvent>
|
|
142
144
|
>("MyPackage/EntityAfterDisableEventHandler");
|
|
143
145
|
|
|
144
146
|
export namespace EntityAfterDisableEventHandler {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
export type Interface = IEventHandler<EntityAfterDisableEvent>;
|
|
148
|
+
export type Event = EntityAfterDisableEvent;
|
|
147
149
|
}
|
|
148
150
|
```
|
|
149
151
|
|
|
@@ -154,29 +156,23 @@ Event classes import payload types and handler abstractions from `abstractions.t
|
|
|
154
156
|
```ts
|
|
155
157
|
// features/disableEntity/events.ts
|
|
156
158
|
import { DomainEvent } from "@webiny/api-core/features/EventPublisher";
|
|
157
|
-
import {
|
|
158
|
-
|
|
159
|
-
EntityAfterDisableEventHandler
|
|
160
|
-
} from "./abstractions.js";
|
|
161
|
-
import type {
|
|
162
|
-
EntityBeforeDisablePayload,
|
|
163
|
-
EntityAfterDisablePayload
|
|
164
|
-
} from "./abstractions.js";
|
|
159
|
+
import { EntityBeforeDisableEventHandler, EntityAfterDisableEventHandler } from "./abstractions.js";
|
|
160
|
+
import type { EntityBeforeDisablePayload, EntityAfterDisablePayload } from "./abstractions.js";
|
|
165
161
|
|
|
166
162
|
export class EntityBeforeDisableEvent extends DomainEvent<EntityBeforeDisablePayload> {
|
|
167
|
-
|
|
163
|
+
eventType = "entity.beforeDisable" as const;
|
|
168
164
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
getHandlerAbstraction() {
|
|
166
|
+
return EntityBeforeDisableEventHandler;
|
|
167
|
+
}
|
|
172
168
|
}
|
|
173
169
|
|
|
174
170
|
export class EntityAfterDisableEvent extends DomainEvent<EntityAfterDisablePayload> {
|
|
175
|
-
|
|
171
|
+
eventType = "entity.afterDisable" as const;
|
|
176
172
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
getHandlerAbstraction() {
|
|
174
|
+
return EntityAfterDisableEventHandler;
|
|
175
|
+
}
|
|
180
176
|
}
|
|
181
177
|
```
|
|
182
178
|
|
|
@@ -188,35 +184,35 @@ import { EventPublisher } from "@webiny/api-core/features/EventPublisher";
|
|
|
188
184
|
import { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js";
|
|
189
185
|
|
|
190
186
|
class DisableEntityUseCase implements UseCaseAbstraction.Interface {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
187
|
+
constructor(
|
|
188
|
+
private eventPublisher: EventPublisher.Interface,
|
|
189
|
+
private getEntityById: GetEntityByIdUseCase.Interface,
|
|
190
|
+
private updateEntity: UpdateEntityUseCase.Interface
|
|
191
|
+
) {}
|
|
196
192
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
193
|
+
async execute(entityId: string): Promise<Result<void, UseCaseAbstraction.Error>> {
|
|
194
|
+
const getResult = await this.getEntityById.execute(entityId);
|
|
195
|
+
if (getResult.isFail()) return Result.fail(getResult.error);
|
|
200
196
|
|
|
201
|
-
|
|
197
|
+
const entity = getResult.value;
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
// Publish BEFORE event (can be intercepted to reject)
|
|
200
|
+
await this.eventPublisher.publish(new EntityBeforeDisableEvent({ entity }));
|
|
205
201
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
202
|
+
// Perform the operation
|
|
203
|
+
const updateResult = await this.updateEntity.execute(entityId, { status: "disabled" });
|
|
204
|
+
if (updateResult.isFail()) return Result.fail(updateResult.error);
|
|
209
205
|
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
// Publish AFTER event (for side effects)
|
|
207
|
+
await this.eventPublisher.publish(new EntityAfterDisableEvent({ entity: updateResult.value }));
|
|
212
208
|
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
return Result.ok();
|
|
210
|
+
}
|
|
215
211
|
}
|
|
216
212
|
|
|
217
213
|
export default UseCaseAbstraction.createImplementation({
|
|
218
|
-
|
|
219
|
-
|
|
214
|
+
implementation: DisableEntityUseCase,
|
|
215
|
+
dependencies: [EventPublisher, GetEntityByIdUseCase, UpdateEntityUseCase]
|
|
220
216
|
});
|
|
221
217
|
```
|
|
222
218
|
|
|
@@ -233,23 +229,23 @@ import { CleanupService } from "../cleanupService/abstractions.js";
|
|
|
233
229
|
import { MY_MODEL_ID } from "~/shared/constants.js";
|
|
234
230
|
|
|
235
231
|
class CleanupOnEntryDeleteHandler implements EntryAfterDeleteEventHandler.Interface {
|
|
236
|
-
|
|
232
|
+
constructor(private cleanupService: CleanupService.Interface) {}
|
|
237
233
|
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
async handle(event: EntryAfterDeleteEventHandler.Event): Promise<void> {
|
|
235
|
+
const { entry, model } = event.payload;
|
|
240
236
|
|
|
241
|
-
|
|
242
|
-
|
|
237
|
+
// ALWAYS filter by model — handler fires for ALL models
|
|
238
|
+
if (model.modelId !== MY_MODEL_ID) return;
|
|
243
239
|
|
|
244
|
-
|
|
240
|
+
if (!event.payload.permanent) return;
|
|
245
241
|
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
await this.cleanupService.cleanup(entry.entryId);
|
|
243
|
+
}
|
|
248
244
|
}
|
|
249
245
|
|
|
250
246
|
export default EntryAfterDeleteEventHandler.createImplementation({
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
implementation: CleanupOnEntryDeleteHandler,
|
|
248
|
+
dependencies: [CleanupService]
|
|
253
249
|
});
|
|
254
250
|
```
|
|
255
251
|
|
|
@@ -257,11 +253,11 @@ export default EntryAfterDeleteEventHandler.createImplementation({
|
|
|
257
253
|
|
|
258
254
|
## Event Naming Conventions
|
|
259
255
|
|
|
260
|
-
| Artifact
|
|
261
|
-
|
|
|
262
|
-
| `eventType`
|
|
263
|
-
| Handler abstraction | `{Entity}{Before\|After}{Action}EventHandler`
|
|
264
|
-
| Event class
|
|
256
|
+
| Artifact | Pattern | Example |
|
|
257
|
+
| ------------------- | ------------------------------------------------ | --------------------------------- |
|
|
258
|
+
| `eventType` | `"entity.beforeAction"` / `"entity.afterAction"` | `"tenant.beforeDisable"` |
|
|
259
|
+
| Handler abstraction | `{Entity}{Before\|After}{Action}EventHandler` | `TenantBeforeDisableEventHandler` |
|
|
260
|
+
| Event class | `{Entity}{Before\|After}{Action}Event` | `TenantBeforeDisableEvent` |
|
|
265
261
|
|
|
266
262
|
## Registration
|
|
267
263
|
|
|
@@ -106,7 +106,6 @@ reply.code(201).header("X-Custom", "value").send({ created: true });
|
|
|
106
106
|
The `<Api.Route>` extension does two things at build/deploy time:
|
|
107
107
|
|
|
108
108
|
1. **Build time** — injects two entries into `apps/api/graphql/src/extensions.ts`:
|
|
109
|
-
|
|
110
109
|
- A `createContextPlugin` that registers your handler in the DI container
|
|
111
110
|
- A `createRoute` that registers the Fastify route with the hardcoded `path` and `method`
|
|
112
111
|
|