@webiny/mcp 6.1.0 → 6.2.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/index.d.ts CHANGED
@@ -4,3 +4,5 @@ export { startMcpServer } from "./cli/McpServer.js";
4
4
  export type { IMcpServerParams } from "./cli/McpServer.js";
5
5
  export { configureMcp } from "./cli/ConfigureMcp.js";
6
6
  export type { IConfigureMcpParams } from "./cli/ConfigureMcp.js";
7
+ export { discoverAgents, discoverPresets } from "./agents/discover.js";
8
+ export type { AgentPreset, AgentModule } from "./agents/types.js";
package/index.js CHANGED
@@ -5,5 +5,7 @@ export { ConsoleUi } from "./ui.js";
5
5
  // Core functions
6
6
  export { startMcpServer } from "./cli/McpServer.js";
7
7
  export { configureMcp } from "./cli/ConfigureMcp.js";
8
+ // Agent discovery
9
+ export { discoverAgents, discoverPresets } from "./agents/discover.js";
8
10
 
9
11
  //# sourceMappingURL=index.js.map
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"names":["ConsoleUi","startMcpServer","configureMcp"],"sources":["index.ts"],"sourcesContent":["// Ui interface and console implementation\nexport type { IUi } from \"./ui.js\";\nexport { ConsoleUi } from \"./ui.js\";\n\n// Core functions\nexport { startMcpServer } from \"./cli/McpServer.js\";\nexport type { IMcpServerParams } from \"./cli/McpServer.js\";\nexport { configureMcp } from \"./cli/ConfigureMcp.js\";\nexport type { IConfigureMcpParams } from \"./cli/ConfigureMcp.js\";\n"],"mappings":"AAAA;;AAEA,SAASA,SAAS;;AAElB;AACA,SAASC,cAAc;AAEvB,SAASC,YAAY","ignoreList":[]}
1
+ {"version":3,"names":["ConsoleUi","startMcpServer","configureMcp","discoverAgents","discoverPresets"],"sources":["index.ts"],"sourcesContent":["// Ui interface and console implementation\nexport type { IUi } from \"./ui.js\";\nexport { ConsoleUi } from \"./ui.js\";\n\n// Core functions\nexport { startMcpServer } from \"./cli/McpServer.js\";\nexport type { IMcpServerParams } from \"./cli/McpServer.js\";\nexport { configureMcp } from \"./cli/ConfigureMcp.js\";\nexport type { IConfigureMcpParams } from \"./cli/ConfigureMcp.js\";\n\n// Agent discovery\nexport { discoverAgents, discoverPresets } from \"./agents/discover.js\";\nexport type { AgentPreset, AgentModule } from \"./agents/types.js\";\n"],"mappings":"AAAA;;AAEA,SAASA,SAAS;;AAElB;AACA,SAASC,cAAc;AAEvB,SAASC,YAAY;AAGrB;AACA,SAASC,cAAc,EAAEC,eAAe","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/mcp",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -18,14 +18,14 @@
18
18
  "directory": "dist"
19
19
  },
20
20
  "dependencies": {
21
- "@modelcontextprotocol/sdk": "1.28.0",
21
+ "@modelcontextprotocol/sdk": "1.29.0",
22
22
  "front-matter": "4.0.2",
23
23
  "zod": "4.3.6"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/lodash": "4.17.24",
27
27
  "@types/ncp": "2.0.8",
28
- "@webiny/build-tools": "6.1.0",
28
+ "@webiny/build-tools": "6.2.0",
29
29
  "execa": "5.1.1",
30
30
  "tsx": "4.21.0",
31
31
  "typescript": "5.9.3"
@@ -33,5 +33,5 @@
33
33
  "scripts": {
34
34
  "prepublishOnly": "bash ./prepublishOnly.sh"
35
35
  },
36
- "gitHead": "65e0ac1889b3392c99b8cac6cde508e1e831c715"
36
+ "gitHead": "3d3148358b6febbc857371930871743bec3b3939"
37
37
  }
@@ -2,82 +2,68 @@
2
2
  name: webiny-admin-permissions
3
3
  context: webiny-extensions
4
4
  description: >
5
- Admin-side permission UI registration using Security.Permissions. Use this skill when
6
- adding permission controls to the admin UI — schema-based auto-generated forms, custom
7
- permission UIs with usePermissionValue/usePermissionForm, entity dependencies, and
8
- the Security.Permissions component props. Covers both simple apps and complex
9
- multi-entity permission schemas.
5
+ Admin-side permission UI registration and DI-backed permission checking.
6
+ Use this skill when adding permission controls to the admin UI — schema-based
7
+ auto-generated forms, injectable permissions via createPermissionsAbstraction/
8
+ createPermissionsFeature, typed hooks (createUsePermissions), the HasPermission
9
+ component (createHasPermission), and the Security.Permissions component props.
10
+ Covers both simple apps and complex multi-entity permission schemas.
10
11
  ---
11
12
 
12
- # Admin Permissions UI
13
+ # Admin Permissions
13
14
 
14
15
  ## Overview
15
16
 
16
- Register permissions via `AdminConfig` + `Security.Permissions`. The framework auto-generates the UI from a schema and handles serialization. No form code needed for most apps.
17
+ Permissions follow three layers: **domain** (schema), **features** (DI artifacts + registration), and **presentation** (hooks, components, UI config). The framework auto-generates the permission UI from a schema and provides injectable permission checking via the DI container.
17
18
 
18
- ## Schema-Based (Auto-Generated UI)
19
+ ## Layer 1: Domain — Permission Schema
19
20
 
20
- ```tsx
21
- import React from "react";
22
- import { AdminConfig } from "@webiny/app-admin";
23
- import { ReactComponent as Icon } from "@webiny/icons/shield.svg";
21
+ Define the schema in `src/domain/permissionsSchema.ts`:
24
22
 
25
- const { Security } = AdminConfig;
23
+ ```ts
24
+ import { createPermissionSchema } from "webiny/admin/security";
26
25
 
27
- export const MyPermission = () => {
28
- return (
29
- <AdminConfig>
30
- <Security.Permissions
31
- name="store-manager"
32
- title="Store Manager"
33
- description="Manage Store Manager permissions."
34
- icon={<Icon />}
35
- schema={{
36
- prefix: "sm",
37
- fullAccess: { name: "sm.*" },
38
- entities: [
39
- {
40
- id: "product",
41
- title: "Products",
42
- permission: "sm.product",
43
- scopes: ["full", "own"],
44
- actions: [
45
- { name: "rwd" },
46
- { name: "pw" },
47
- { name: "import", label: "Import products" },
48
- { name: "export", label: "Export products" }
49
- ]
50
- },
51
- {
52
- id: "category",
53
- title: "Categories",
54
- permission: "sm.category",
55
- scopes: ["full"],
56
- actions: [{ name: "rwd" }]
57
- },
58
- {
59
- id: "settings",
60
- title: "Settings",
61
- permission: "sm.settings",
62
- scopes: ["full"]
63
- }
64
- ]
65
- }}
66
- />
67
- </AdminConfig>
68
- );
69
- };
26
+ export const SM_PERMISSIONS_SCHEMA = createPermissionSchema({
27
+ prefix: "sm",
28
+ fullAccess: true,
29
+ entities: [
30
+ {
31
+ id: "product",
32
+ title: "Products",
33
+ permission: "sm.product",
34
+ scopes: ["full", "own"],
35
+ actions: [
36
+ { name: "rwd" },
37
+ { name: "pw" },
38
+ { name: "import", label: "Import products" },
39
+ { name: "export", label: "Export products" }
40
+ ]
41
+ },
42
+ {
43
+ id: "category",
44
+ title: "Categories",
45
+ permission: "sm.category",
46
+ scopes: ["full"],
47
+ actions: [{ name: "rwd" }]
48
+ },
49
+ {
50
+ id: "settings",
51
+ title: "Settings",
52
+ permission: "sm.settings",
53
+ scopes: ["full"]
54
+ }
55
+ ]
56
+ });
70
57
  ```
71
58
 
72
- Render `<MyPermission />` anywhere in your app's extension component.
73
-
74
- ## Schema Reference
59
+ ### Schema Reference
75
60
 
76
- | Field | Type | Required | Description |
77
- | ------------ | -------------------- | -------- | -------------------------------------------------------------- |
78
- | `prefix` | `string` | Yes | Permission prefix (e.g., `"sm"`) |
79
- | `fullAccess` | `{ name: string }` | Yes | Permission emitted on "Full access" (e.g., `{ name: "sm.*" }`) |
80
- | `entities` | `EntityDefinition[]` | No | Entity definitions. Omit for binary full/no access. |
61
+ | Field | Type | Required | Description |
62
+ | ---------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
63
+ | `prefix` | `string` | Yes | Permission prefix (e.g., `"sm"`) |
64
+ | `fullAccess` | `true \| object` | Yes | `true` for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., `{ canForceUnlock: true }`). |
65
+ | `readOnlyAccess` | `boolean` | No | Whether to show a "Read-only access" option |
66
+ | `entities` | `EntityDefinition[]` | No | Entity definitions. Omit for binary full/no access. |
81
67
 
82
68
  ### Entity Definition
83
69
 
@@ -110,19 +96,170 @@ Child entities can depend on a parent. If the parent lacks the required action,
110
96
  }
111
97
  ```
112
98
 
99
+ ### Simple Apps (No Entities)
100
+
101
+ Omit `entities` for binary full/no access:
102
+
103
+ ```ts
104
+ export const MA_PERMISSIONS_SCHEMA = createPermissionSchema({
105
+ prefix: "ma",
106
+ fullAccess: true
107
+ });
108
+ ```
109
+
113
110
  ---
114
111
 
115
- ## Simple Apps (No Entities)
112
+ ## Layer 2: Features — DI Artifacts + Registration
116
113
 
117
- Omit `entities` for binary full/no access:
114
+ ### Abstraction (`src/features/permissions/abstractions.ts`)
115
+
116
+ ```ts
117
+ import { createPermissionsAbstraction } from "webiny/admin/security";
118
+ import type { Permissions } from "webiny/admin/security";
119
+ import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
120
+
121
+ export const SmPermissions = createPermissionsAbstraction(SM_PERMISSIONS_SCHEMA);
122
+
123
+ export namespace SmPermissions {
124
+ export type Interface = Permissions<typeof SM_PERMISSIONS_SCHEMA>;
125
+ }
126
+ ```
127
+
128
+ ### Feature (`src/features/permissions/feature.ts`)
129
+
130
+ ```ts
131
+ import { createPermissionsFeature } from "webiny/admin/security";
132
+ import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
133
+ import { SmPermissions } from "./abstractions.js";
134
+
135
+ export const SmPermissionsFeature = createPermissionsFeature(SM_PERMISSIONS_SCHEMA, SmPermissions);
136
+ ```
137
+
138
+ ### Extension Registration
139
+
140
+ Register the feature and the permission UI in your extension component:
141
+
142
+ ```tsx
143
+ import { AdminConfig, RegisterFeature } from "webiny/admin/security";
144
+ import { ReactComponent as Icon } from "@webiny/icons/shield.svg";
145
+ import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
146
+ import { SmPermissionsFeature } from "~/features/permissions/feature.js";
147
+
148
+ const { Security } = AdminConfig;
149
+
150
+ export const Extension = () => {
151
+ return (
152
+ <>
153
+ <RegisterFeature feature={SmPermissionsFeature} />
154
+ <AdminConfig>
155
+ <Security.Permissions
156
+ name="store-manager"
157
+ title="Store Manager"
158
+ description="Manage Store Manager permissions."
159
+ icon={<Icon />}
160
+ schema={SM_PERMISSIONS_SCHEMA}
161
+ />
162
+ {/* Routes, menus, etc. */}
163
+ </AdminConfig>
164
+ </>
165
+ );
166
+ };
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Layer 3: Presentation — Hooks & Components
172
+
173
+ ### `usePermissions` Hook
174
+
175
+ ```ts
176
+ // src/presentation/security/usePermissions.ts
177
+ import { createUsePermissions } from "webiny/admin/security";
178
+ import { SmPermissions } from "~/features/permissions/abstractions.js";
179
+
180
+ export const usePermissions = createUsePermissions(SmPermissions);
181
+ ```
182
+
183
+ Usage:
184
+
185
+ ```ts
186
+ const permissions = usePermissions();
187
+
188
+ permissions.canAccess("product"); // has any access
189
+ permissions.canRead("product"); // rwd includes "r"
190
+ permissions.canCreate("product"); // rwd includes "w"
191
+ permissions.canEdit("product", item); // rwd includes "w", respects own scope
192
+ permissions.canDelete("product"); // rwd includes "d"
193
+ permissions.canPublish("product"); // pw includes "p"
194
+ permissions.canUnpublish("product"); // pw includes "u"
195
+ permissions.canAction("import", "product"); // custom boolean flag
196
+ ```
197
+
198
+ Entity IDs are fully typed — `canRead("bogus")` produces a type error.
199
+
200
+ ### `HasPermission` Component
118
201
 
119
202
  ```tsx
120
- <Security.Permissions
121
- name="my-app"
122
- title="My App"
123
- description="Manage My App access permissions."
124
- schema={{ prefix: "ma", fullAccess: { name: "ma.*" } }}
125
- />
203
+ // src/presentation/security/HasPermission.tsx
204
+ import { createHasPermission } from "webiny/admin/security";
205
+ import { SmPermissions } from "~/features/permissions/abstractions.js";
206
+ import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
207
+
208
+ export const HasPermission = createHasPermission(SmPermissions, SM_PERMISSIONS_SCHEMA);
209
+ ```
210
+
211
+ Usage in JSX:
212
+
213
+ ```tsx
214
+ <HasPermission entity="product">
215
+ {/* Rendered if user can access "product" */}
216
+ </HasPermission>
217
+
218
+ <HasPermission any={["product", "category"]}>
219
+ {/* Rendered if user can access ANY of these entities */}
220
+ </HasPermission>
221
+
222
+ <HasPermission all={["product", "category"]}>
223
+ {/* Rendered if user can access ALL of these entities */}
224
+ </HasPermission>
225
+
226
+ <HasPermission entity="product" action="read">
227
+ {/* Rendered if user canRead("product") */}
228
+ </HasPermission>
229
+
230
+ <HasPermission entity="product" allActions={["read", "publish"]}>
231
+ {/* Rendered if user canRead AND canPublish "product" */}
232
+ </HasPermission>
233
+
234
+ <HasPermission entity="product" someActions={["import", "export"]}>
235
+ {/* Rendered if user can do ANY of these custom actions */}
236
+ </HasPermission>
237
+ ```
238
+
239
+ ---
240
+
241
+ ## DI Injection in Features
242
+
243
+ Inject permissions into feature implementations:
244
+
245
+ ```ts
246
+ import type { SmPermissions } from "~/features/permissions/abstractions.js";
247
+ import { SmPermissions as SmPermissionsAbstraction } from "~/features/permissions/abstractions.js";
248
+
249
+ class SomeFeatureImpl {
250
+ constructor(private permissions: SmPermissions.Interface) {}
251
+
252
+ doSomething() {
253
+ if (this.permissions.canEdit("product")) {
254
+ // ...
255
+ }
256
+ }
257
+ }
258
+
259
+ const SomeFeature = SomeAbstraction.createImplementation({
260
+ implementation: SomeFeatureImpl,
261
+ dependencies: [SmPermissionsAbstraction]
262
+ });
126
263
  ```
127
264
 
128
265
  ---
@@ -141,12 +278,29 @@ Omit `entities` for binary full/no access:
141
278
 
142
279
  ---
143
280
 
281
+ ## File Structure
282
+
283
+ ```
284
+ src/
285
+ ├── domain/
286
+ │ └── permissionsSchema.ts # createPermissionSchema()
287
+ ├── features/
288
+ │ └── permissions/
289
+ │ ├── abstractions.ts # createPermissionsAbstraction() + namespace type
290
+ │ └── feature.ts # createPermissionsFeature()
291
+ ├── presentation/
292
+ │ └── security/
293
+ │ ├── usePermissions.ts # createUsePermissions()
294
+ │ └── HasPermission.tsx # createHasPermission()
295
+ └── Extension.tsx # RegisterFeature + Security.Permissions
296
+ ```
297
+
144
298
  ## Matching API-Side Permissions
145
299
 
146
300
  The admin schema and the API-side `createPermissions` schema should use the **same prefix, entity IDs, and action names**. This ensures the permissions emitted by the admin UI are correctly evaluated by the API.
147
301
 
148
302
  ```
149
- Admin: schema={{ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] }}
303
+ Admin: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
150
304
  API: createPermissions({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
151
305
  ```
152
306
 
@@ -176,56 +176,25 @@ Create custom forms for Website Builder page types using Webiny's form component
176
176
  ```tsx
177
177
  // extensions/customPageTypes/RetailPageForm.tsx
178
178
  import React from "react";
179
- import { Grid, Input, Select } from "webiny/admin/ui";
180
- import { pagePathFromTitle } from "webiny/admin/website-builder";
181
- import type { FormApi } from "webiny/admin/form";
182
- import { Bind, UnsetOnUnmount, useForm, validation } from "webiny/admin/form";
179
+ import { PageListConfig } from "webiny/admin/website-builder/page/list";
180
+ import { Bind, UnsetOnUnmount, validation } from "webiny/admin/form";
183
181
 
184
- const generatePath = (form: FormApi) => () => {
185
- const title = form.getValue("properties.title");
186
- const language = form.getValue("extensions.language");
187
-
188
- const titlePath = pagePathFromTitle(title ?? "");
189
- const parts = [language, titlePath].filter(Boolean);
190
-
191
- form.setValue("properties.path", `/${parts.join("/")}`);
192
- };
182
+ const { PageType } = PageListConfig;
193
183
 
194
184
  export const RetailPageForm = () => {
195
185
  const form = useForm();
196
186
 
197
187
  return (
198
188
  <>
189
+ {/* Mount the default page form fields. */}
190
+ <PageType.Language />
191
+ <PageType.Title />
192
+ <PageType.Path />
193
+ {/* Add custom fields.*/}
199
194
  <Grid.Column span={12}>
200
- <UnsetOnUnmount name={"properties.title"}>
201
- <Bind name={"properties.title"} validators={[validation.create("required")]}>
202
- <Input label={"Title"} onBlur={generatePath(form)} />
203
- </Bind>
204
- </UnsetOnUnmount>
205
- </Grid.Column>
206
- <Grid.Column span={12}>
207
- <UnsetOnUnmount name={"extensions.language"}>
208
- <Bind
209
- name={"extensions.language"}
210
- validators={[validation.create("required")]}
211
- afterChange={generatePath(form)}
212
- >
213
- <Select
214
- placeholder={"Select a language"}
215
- label={"Language"}
216
- options={[
217
- { label: "English", value: "en" },
218
- { label: "German", value: "de" },
219
- { label: "French", value: "fr" }
220
- ]}
221
- />
222
- </Bind>
223
- </UnsetOnUnmount>
224
- </Grid.Column>
225
- <Grid.Column span={12}>
226
- <UnsetOnUnmount name={"properties.path"}>
227
- <Bind name={"properties.path"} validators={[validation.create("required")]}>
228
- <Input label={"Path"} />
195
+ <UnsetOnUnmount name={"extensions.customField"}>
196
+ <Bind name={"extensions.customField"} validators={[validation.create("required")]}>
197
+ <Input label={"Custom Field"} />
229
198
  </Bind>
230
199
  </UnsetOnUnmount>
231
200
  </Grid.Column>
@@ -426,7 +426,7 @@ export const CreateEntityFeature = createFeature({
426
426
  A deployed API must **NEVER** use `process.env` to read configuration. All configuration flows through `BuildParams` via DI:
427
427
 
428
428
  ```ts
429
- import { BuildParams } from "webiny/api/build-params";
429
+ import { BuildParams } from "webiny/api";
430
430
 
431
431
  class MyServiceImpl implements MyService.Interface {
432
432
  constructor(private buildParams: BuildParams.Interface) {}
@@ -672,6 +672,7 @@ Creates a feature definition that the framework loads as an extension.
672
672
  - **webiny-api-permissions** — Schema-based permissions, CRUD authorization patterns, own-record scoping, testing
673
673
  - **webiny-event-handler-pattern** — EventHandler lifecycle, domain event definition and publishing, handler abstractions
674
674
  - **webiny-custom-graphql-api** — GraphQL schema creation, dynamic inputs, namespaced mutations
675
+ - **webiny-http-route** — Custom HTTP endpoints via `Api.Route` and `Route.Interface`
675
676
  - **webiny-v5-to-v6-migration** — Side-by-side migration patterns for AI agents
676
677
  - **webiny-full-stack-architect** — Top-level component, shared domain layer, BuildParam declarations
677
678
  - **webiny-dependency-injection** — The `createImplementation` DI pattern and injectable services
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: webiny-http-route
3
+ context: webiny-extensions
4
+ description: >
5
+ Adding custom HTTP routes to the API using Api.Route and Route.Interface.
6
+ Use this skill when the developer wants to expose a custom HTTP endpoint (GET, POST, PUT, etc.)
7
+ on the API Gateway alongside the GraphQL handler, implement Route.Interface with full DI support,
8
+ or register a custom HTTP handler in webiny.config.tsx.
9
+ ---
10
+
11
+ # Custom HTTP Routes
12
+
13
+ ## TL;DR
14
+
15
+ Add a custom HTTP route by implementing `Route.Interface` and registering it with `<Api.Route>` in `webiny.config.tsx`. The `path` and `method` props configure API Gateway and Fastify route registration. Your handler receives a framework-agnostic `Route.Request` and `Route.Reply` and supports full DI (Logger, BuildParams, UseCases, etc.).
16
+
17
+ **YOU MUST include the full file path with the `.ts` extension in the `src` prop.** For example, use `src={"/extensions/MyRoute.ts"}`, NOT `src={"/extensions/MyRoute"}`. Omitting the file extension will cause a build failure.
18
+
19
+ **YOU MUST use `export default` for the `createImplementation()` call** when the file is targeted directly by a `src` prop. Named exports cause build failures here.
20
+
21
+ ## The Route Pattern
22
+
23
+ ```typescript
24
+ // extensions/MyRoute.ts
25
+ import { Route } from "webiny/api/route";
26
+ import { Logger } from "webiny/api/logger";
27
+ import { BuildParams } from "webiny/api/build-params";
28
+
29
+ class MyRouteImpl implements Route.Interface {
30
+ constructor(
31
+ private logger: Logger.Interface,
32
+ private buildParams: BuildParams.Interface
33
+ ) {}
34
+
35
+ async execute(request: Route.Request, reply: Route.Reply): Promise<void> {
36
+ this.logger.info("Handling request", { url: request.url });
37
+
38
+ const config = this.buildParams.get<string>("MY_CONFIG");
39
+
40
+ reply.code(200).send({ status: "ok", config });
41
+ }
42
+ }
43
+
44
+ export default Route.createImplementation({
45
+ implementation: MyRouteImpl,
46
+ dependencies: [Logger, BuildParams]
47
+ });
48
+ ```
49
+
50
+ Register in `webiny.config.tsx`:
51
+
52
+ ```tsx
53
+ <Api.Route method={"POST"} path={"/my-route"} src={"/extensions/MyRoute.ts"} />
54
+ ```
55
+
56
+ ## Props Reference
57
+
58
+ | Prop | Type | Required | Description |
59
+ | ----------- | -------- | -------- | --------------------------------------------------------------------------- |
60
+ | `path` | `string` | Yes | Route path — must start with `/` |
61
+ | `method` | `string` | Yes | HTTP method (see list below) |
62
+ | `src` | `string` | Yes | Path to the handler file (must include `.ts` extension) |
63
+ | `routeName` | `string` | No | Pulumi resource name (kebab-case). Auto-derived from path+method if omitted |
64
+
65
+ ## Supported HTTP Methods
66
+
67
+ `DELETE`, `GET`, `HEAD`, `PATCH`, `POST`, `PUT`, `OPTIONS`, `ANY`
68
+
69
+ Use `ANY` to match all HTTP methods on a given path.
70
+
71
+ ## Route.Request / Route.Reply Interfaces
72
+
73
+ These are framework-agnostic — no Fastify types leak into user code.
74
+
75
+ ### `Route.Request`
76
+
77
+ ```ts
78
+ interface Route.Request {
79
+ body: unknown;
80
+ headers: Record<string, string | string[] | undefined>;
81
+ method: string;
82
+ url: string;
83
+ params: unknown; // path parameters, e.g. { id: "abc" }
84
+ query: unknown; // query string parameters
85
+ }
86
+ ```
87
+
88
+ ### `Route.Reply`
89
+
90
+ ```ts
91
+ interface Route.Reply {
92
+ code(statusCode: number): this; // set HTTP status code
93
+ send(data?: unknown): void; // send response body
94
+ header(key: string, value: unknown): this;
95
+ }
96
+ ```
97
+
98
+ Chaining example:
99
+
100
+ ```ts
101
+ reply.code(201).header("X-Custom", "value").send({ created: true });
102
+ ```
103
+
104
+ ## How It Works
105
+
106
+ The `<Api.Route>` extension does two things at build/deploy time:
107
+
108
+ 1. **Build time** — injects two entries into `apps/api/graphql/src/extensions.ts`:
109
+
110
+ - A `createContextPlugin` that registers your handler in the DI container
111
+ - A `createRoute` that registers the Fastify route with the hardcoded `path` and `method`
112
+
113
+ 2. **Deploy time (Pulumi)** — calls `graphql.addRoute({ name, path, method })` on the `ApiGraphql` module to create the API Gateway route
114
+
115
+ At request time, the handler is resolved from the DI container — so all `dependencies` (Logger, BuildParams, UseCases, etc.) are fully injected.
116
+
117
+ ## Example with Path Parameters
118
+
119
+ ```typescript
120
+ // extensions/GetOrderRoute.ts
121
+ import { Route } from "webiny/api/route";
122
+ import { Logger } from "webiny/api/logger";
123
+
124
+ interface OrderParams {
125
+ orderId: string;
126
+ }
127
+
128
+ class GetOrderRouteImpl implements Route.Interface {
129
+ constructor(private logger: Logger.Interface) {}
130
+
131
+ async execute(request: Route.Request, reply: Route.Reply): Promise<void> {
132
+ const { orderId } = request.params as OrderParams;
133
+ this.logger.info("Fetching order", { orderId });
134
+
135
+ // ... fetch and return order
136
+ reply.code(200).send({ orderId, status: "fulfilled" });
137
+ }
138
+ }
139
+
140
+ export default Route.createImplementation({
141
+ implementation: GetOrderRouteImpl,
142
+ dependencies: [Logger]
143
+ });
144
+ ```
145
+
146
+ ```tsx
147
+ <Api.Route method={"GET"} path={"/orders/{orderId}"} src={"/extensions/GetOrderRoute.ts"} />
148
+ ```
149
+
150
+ ## Key Rules
151
+
152
+ - **`path` and `method` are hardcoded at build time** — the Fastify route is registered before any request arrives. Your handler does not need to specify them.
153
+ - **DI is fully available in `execute()`** — the handler instance is resolved from the container per request, so all dependencies are injected normally.
154
+ - **One handler per file** — each `src` file exports one `Route.createImplementation` result.
155
+ - **Constructor param order must match the `dependencies` array** exactly.
156
+ - **Use `.js` extensions** in all relative imports inside the handler file (ESM).
157
+ - **Do not read `process.env`** at runtime — use `BuildParams` for configuration instead.
158
+
159
+ ## Quick Reference
160
+
161
+ ```
162
+ Import (handler): import { Route } from "webiny/api/route";
163
+ Interface: Route.Interface
164
+ Request type: Route.Request
165
+ Reply type: Route.Reply
166
+ Export: Route.createImplementation({ implementation, dependencies })
167
+ Register: <Api.Route method={"POST"} path={"/my-route"} src={"/extensions/MyRoute.ts"} />
168
+ Deploy: yarn webiny deploy api --env=dev
169
+ ```
170
+
171
+ ## Related Skills
172
+
173
+ - **webiny-api-architect** — DI patterns, Services, UseCases, feature organization
174
+ - **webiny-custom-graphql-api** — Custom GraphQL endpoints (alternative to HTTP)
175
+ - **webiny-dependency-injection** — Injectable services catalog (Logger, BuildParams, etc.)
176
+ - **webiny-infrastructure-extensions** — Pulumi-level infrastructure customization