@jrmc/adonis-mcp 1.0.0-alpha.9 → 1.0.0-beta.10

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.
Files changed (210) hide show
  1. package/README.md +835 -29
  2. package/build/commands/commands.json +1 -1
  3. package/build/commands/inspector.d.ts +17 -0
  4. package/build/commands/inspector.d.ts.map +1 -0
  5. package/build/commands/inspector.js +55 -0
  6. package/build/commands/make/prompt.d.ts +16 -0
  7. package/build/commands/make/prompt.d.ts.map +1 -0
  8. package/build/commands/make/prompt.js +36 -0
  9. package/build/commands/make/resource.d.ts +16 -0
  10. package/build/commands/make/resource.d.ts.map +1 -0
  11. package/build/commands/make/resource.js +33 -0
  12. package/build/commands/make/tool.d.ts.map +1 -1
  13. package/build/commands/make/tool.js +4 -3
  14. package/build/configure.d.ts.map +1 -1
  15. package/build/configure.js +22 -1
  16. package/build/index.d.ts +3 -0
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js +3 -0
  19. package/build/providers/mcp_provider.d.ts +3 -0
  20. package/build/providers/mcp_provider.d.ts.map +1 -1
  21. package/build/providers/mcp_provider.js +45 -12
  22. package/build/providers/vinejs_provider.d.ts +13 -0
  23. package/build/providers/vinejs_provider.d.ts.map +1 -0
  24. package/build/providers/vinejs_provider.js +10 -0
  25. package/build/src/define_config.d.ts.map +1 -1
  26. package/build/src/define_config.js +0 -3
  27. package/build/src/enums/error.d.ts.map +1 -1
  28. package/build/src/enums/error.js +0 -1
  29. package/build/src/request.d.ts +7 -3
  30. package/build/src/request.d.ts.map +1 -1
  31. package/build/src/request.js +6 -2
  32. package/build/src/response.d.ts +24 -23
  33. package/build/src/response.d.ts.map +1 -1
  34. package/build/src/response.js +49 -13
  35. package/build/src/server/annotations/annotations.d.ts +10 -0
  36. package/build/src/server/annotations/annotations.d.ts.map +1 -0
  37. package/build/src/server/annotations/annotations.js +9 -0
  38. package/build/src/server/annotations/audience.d.ts +22 -0
  39. package/build/src/server/annotations/audience.d.ts.map +1 -0
  40. package/build/src/server/annotations/audience.js +28 -0
  41. package/build/src/server/annotations/is_destructive.d.ts +17 -0
  42. package/build/src/server/annotations/is_destructive.d.ts.map +1 -0
  43. package/build/src/server/annotations/is_destructive.js +24 -0
  44. package/build/src/server/annotations/is_idempotent.d.ts +17 -0
  45. package/build/src/server/annotations/is_idempotent.d.ts.map +1 -0
  46. package/build/src/server/annotations/is_idempotent.js +24 -0
  47. package/build/src/server/annotations/is_open_world.d.ts +17 -0
  48. package/build/src/server/annotations/is_open_world.d.ts.map +1 -0
  49. package/build/src/server/annotations/is_open_world.js +24 -0
  50. package/build/src/server/annotations/is_read_only.d.ts +17 -0
  51. package/build/src/server/annotations/is_read_only.d.ts.map +1 -0
  52. package/build/src/server/annotations/is_read_only.js +24 -0
  53. package/build/src/server/annotations/last_modified.d.ts +21 -0
  54. package/build/src/server/annotations/last_modified.d.ts.map +1 -0
  55. package/build/src/server/annotations/last_modified.js +28 -0
  56. package/build/src/server/annotations/priority.d.ts +21 -0
  57. package/build/src/server/annotations/priority.d.ts.map +1 -0
  58. package/build/src/server/annotations/priority.js +31 -0
  59. package/build/src/server/annotations/tool_annotations.d.ts +11 -0
  60. package/build/src/server/annotations/tool_annotations.d.ts.map +1 -0
  61. package/build/src/server/annotations/tool_annotations.js +10 -0
  62. package/build/src/server/contents/audio.d.ts +24 -0
  63. package/build/src/server/contents/audio.d.ts.map +1 -0
  64. package/build/src/server/contents/audio.js +57 -0
  65. package/build/src/server/contents/blob.d.ts +20 -0
  66. package/build/src/server/contents/blob.d.ts.map +1 -0
  67. package/build/src/server/contents/blob.js +42 -0
  68. package/build/src/server/contents/embedded_resource.d.ts +26 -0
  69. package/build/src/server/contents/embedded_resource.d.ts.map +1 -0
  70. package/build/src/server/contents/embedded_resource.js +76 -0
  71. package/build/src/server/contents/error.d.ts +20 -0
  72. package/build/src/server/contents/error.d.ts.map +1 -0
  73. package/build/src/server/contents/error.js +25 -0
  74. package/build/src/server/contents/image.d.ts +24 -0
  75. package/build/src/server/contents/image.d.ts.map +1 -0
  76. package/build/src/server/contents/image.js +57 -0
  77. package/build/src/server/contents/resource_link.d.ts +23 -0
  78. package/build/src/server/contents/resource_link.d.ts.map +1 -0
  79. package/build/src/server/contents/resource_link.js +59 -0
  80. package/build/src/server/contents/structured.d.ts +20 -0
  81. package/build/src/server/contents/structured.d.ts.map +1 -0
  82. package/build/src/server/contents/structured.js +26 -0
  83. package/build/src/server/contents/text.d.ts +24 -0
  84. package/build/src/server/contents/text.d.ts.map +1 -0
  85. package/build/src/server/contents/text.js +61 -0
  86. package/build/src/server/context.d.ts +9 -6
  87. package/build/src/server/context.d.ts.map +1 -1
  88. package/build/src/server/context.js +13 -3
  89. package/build/src/server/contracts/content.d.ts +16 -0
  90. package/build/src/server/contracts/content.d.ts.map +1 -0
  91. package/build/src/server/contracts/content.js +8 -0
  92. package/build/src/server/contracts/context.d.ts +28 -0
  93. package/build/src/server/contracts/context.d.ts.map +1 -0
  94. package/build/src/server/contracts/request.d.ts +12 -0
  95. package/build/src/server/contracts/request.d.ts.map +1 -0
  96. package/build/src/server/contracts/request.js +1 -0
  97. package/build/src/server/contracts/response.d.ts +26 -0
  98. package/build/src/server/contracts/response.d.ts.map +1 -0
  99. package/build/src/{types → server/contracts}/transport.d.ts +2 -2
  100. package/build/src/server/contracts/transport.d.ts.map +1 -0
  101. package/build/src/server/contracts/transport.js +7 -0
  102. package/build/src/server/controllers/mcp_controller.d.ts.map +1 -1
  103. package/build/src/server/controllers/mcp_controller.js +0 -3
  104. package/build/src/server/exceptions/jsonrpc_error.d.ts +7 -0
  105. package/build/src/server/exceptions/jsonrpc_error.d.ts.map +1 -0
  106. package/build/src/server/exceptions/jsonrpc_error.js +10 -0
  107. package/build/src/server/exceptions/jsonrpc_exception.d.ts +6 -5
  108. package/build/src/server/exceptions/jsonrpc_exception.d.ts.map +1 -1
  109. package/build/src/server/exceptions/jsonrpc_exception.js +18 -9
  110. package/build/src/server/mcp_bouncer.d.ts +115 -0
  111. package/build/src/server/mcp_bouncer.d.ts.map +1 -0
  112. package/build/src/server/mcp_bouncer.js +123 -0
  113. package/build/src/server/methods/call_tool.d.ts +2 -2
  114. package/build/src/server/methods/call_tool.d.ts.map +1 -1
  115. package/build/src/server/methods/call_tool.js +53 -18
  116. package/build/src/server/methods/completion.d.ts +12 -0
  117. package/build/src/server/methods/completion.d.ts.map +1 -0
  118. package/build/src/server/methods/completion.js +46 -0
  119. package/build/src/server/methods/get_prompt.d.ts +2 -2
  120. package/build/src/server/methods/get_prompt.d.ts.map +1 -1
  121. package/build/src/server/methods/get_prompt.js +56 -2
  122. package/build/src/server/methods/initialize.d.ts +2 -2
  123. package/build/src/server/methods/initialize.d.ts.map +1 -1
  124. package/build/src/server/methods/initialize.js +4 -1
  125. package/build/src/server/methods/list_prompts.d.ts +2 -2
  126. package/build/src/server/methods/list_prompts.d.ts.map +1 -1
  127. package/build/src/server/methods/list_prompts.js +13 -3
  128. package/build/src/server/methods/list_resource_templates.d.ts +12 -0
  129. package/build/src/server/methods/list_resource_templates.d.ts.map +1 -0
  130. package/build/src/server/methods/list_resource_templates.js +23 -0
  131. package/build/src/server/methods/list_resources.d.ts +2 -2
  132. package/build/src/server/methods/list_resources.d.ts.map +1 -1
  133. package/build/src/server/methods/list_resources.js +13 -3
  134. package/build/src/server/methods/list_tools.d.ts +2 -2
  135. package/build/src/server/methods/list_tools.d.ts.map +1 -1
  136. package/build/src/server/methods/list_tools.js +5 -28
  137. package/build/src/server/methods/ping.d.ts +2 -2
  138. package/build/src/server/methods/ping.d.ts.map +1 -1
  139. package/build/src/server/methods/ping.js +1 -1
  140. package/build/src/server/methods/read_resource.d.ts +2 -2
  141. package/build/src/server/methods/read_resource.d.ts.map +1 -1
  142. package/build/src/server/methods/read_resource.js +29 -2
  143. package/build/src/server/pagination/cursor_paginator.d.ts +7 -7
  144. package/build/src/server/pagination/cursor_paginator.d.ts.map +1 -1
  145. package/build/src/server/pagination/cursor_paginator.js +15 -15
  146. package/build/src/server/prompt.d.ts +28 -0
  147. package/build/src/server/prompt.d.ts.map +1 -0
  148. package/build/src/server/prompt.js +32 -0
  149. package/build/src/server/requests/request_validator.d.ts +15 -0
  150. package/build/src/server/requests/request_validator.d.ts.map +1 -0
  151. package/build/src/server/requests/request_validator.js +25 -0
  152. package/build/src/server/resource.d.ts +23 -0
  153. package/build/src/server/resource.d.ts.map +1 -0
  154. package/build/src/server/resource.js +40 -0
  155. package/build/src/server/tool.d.ts +19 -9
  156. package/build/src/server/tool.d.ts.map +1 -1
  157. package/build/src/server/tool.js +21 -0
  158. package/build/src/server/transports/fake_transport.d.ts +19 -1
  159. package/build/src/server/transports/fake_transport.d.ts.map +1 -1
  160. package/build/src/server/transports/fake_transport.js +28 -1
  161. package/build/src/server/transports/http_transport.d.ts +4 -3
  162. package/build/src/server/transports/http_transport.d.ts.map +1 -1
  163. package/build/src/server/transports/http_transport.js +14 -8
  164. package/build/src/server/transports/stdio_transport.d.ts +2 -2
  165. package/build/src/server/transports/stdio_transport.d.ts.map +1 -1
  166. package/build/src/server/transports/stdio_transport.js +0 -3
  167. package/build/src/server.d.ts +6 -3
  168. package/build/src/server.d.ts.map +1 -1
  169. package/build/src/server.js +24 -11
  170. package/build/src/types/config.d.ts +1 -0
  171. package/build/src/types/config.d.ts.map +1 -1
  172. package/build/src/types/content.d.ts +35 -0
  173. package/build/src/types/content.d.ts.map +1 -0
  174. package/build/src/types/content.js +7 -0
  175. package/build/src/types/context.d.ts +31 -18
  176. package/build/src/types/context.d.ts.map +1 -1
  177. package/build/src/types/jsonrpc.d.ts +406 -0
  178. package/build/src/types/jsonrpc.d.ts.map +1 -0
  179. package/build/src/types/jsonrpc.js +7 -0
  180. package/build/src/types/method.d.ts +22 -12
  181. package/build/src/types/method.d.ts.map +1 -1
  182. package/build/src/types/request.d.ts +24 -35
  183. package/build/src/types/request.d.ts.map +1 -1
  184. package/build/src/types/response.d.ts +16 -38
  185. package/build/src/types/response.d.ts.map +1 -1
  186. package/build/src/utils/find_resource_pattern.d.ts +21 -0
  187. package/build/src/utils/find_resource_pattern.d.ts.map +1 -0
  188. package/build/src/utils/find_resource_pattern.js +35 -0
  189. package/build/src/utils/stdio.d.ts +2 -3
  190. package/build/src/utils/stdio.d.ts.map +1 -1
  191. package/build/src/utils/stdio.js +7 -7
  192. package/build/src/utils/uri_template.d.ts +25 -0
  193. package/build/src/utils/uri_template.d.ts.map +1 -0
  194. package/build/src/utils/uri_template.js +241 -0
  195. package/build/stubs/config.ts.stub +1 -1
  196. package/build/stubs/main.d.ts.map +1 -1
  197. package/build/stubs/main.js +1 -2
  198. package/build/stubs/make/mcp/prompts/main.ts.stub +32 -16
  199. package/build/stubs/make/mcp/prompts/with_vine.ts.stub +40 -0
  200. package/build/stubs/make/mcp/resources/main.ts.stub +13 -19
  201. package/build/stubs/make/mcp/tools/main.ts.stub +11 -13
  202. package/build/stubs/make/mcp/tools/with_vine.ts.stub +37 -0
  203. package/build/stubs/make/middleware/mcp_middleware.ts.stub +49 -0
  204. package/build/tsconfig.tsbuildinfo +1 -1
  205. package/package.json +37 -7
  206. package/build/src/types/notification.d.ts +0 -14
  207. package/build/src/types/notification.d.ts.map +0 -1
  208. package/build/src/types/transport.d.ts.map +0 -1
  209. /package/build/src/{types/notification.js → server/contracts/context.js} +0 -0
  210. /package/build/src/{types/transport.js → server/contracts/response.js} +0 -0
package/README.md CHANGED
@@ -5,14 +5,38 @@
5
5
 
6
6
  AdonisJS MCP - Server MCP for your AdonisJS applications.
7
7
 
8
+ > **Note:** This documentation has been generated by AI and has not been fully verified yet. Please report any inaccuracies or issues you encounter.
9
+
10
+ ## Links
11
+
12
+ [View documentation](https://adonis-mcp.jrmc.dev/)
13
+
8
14
  ## Roadmap
9
15
 
16
+ - [x] MCP tools support
17
+ - [x] MCP resources support
10
18
  - [x] MCP prompts support
11
- - [ ] MCP resources support
12
- - [ ] MCP prompts support
13
- - [ ] Alternative transports support (SSE, stdio)
19
+ - [x] HTTP transport
20
+ - [x] Stdio transport
21
+ - [x] Fake transport (for testing)
14
22
  - [x] Advanced pagination support
15
- - [ ] Automatic schema validation with Vine ??
23
+ - [x] Meta support
24
+ - [x] Annotations
25
+ - [x] Completion
26
+ - [x] Inspector
27
+ - [x] Session
28
+ - [x] Documentation
29
+ - [x] Inject support
30
+ - [x] VineJs integration
31
+ - [x] JSON Schema with VineJs
32
+ - [x] Bounce integration
33
+ - [x] Auth helpers
34
+ - [x] Starter kit
35
+ - [x] Demo applications
36
+ - [ ] Events
37
+ - [ ] Logger
38
+ - [ ] Alternative transports (SSE)
39
+ - [ ] Output tool
16
40
 
17
41
  ## Installation & Configuration
18
42
 
@@ -28,10 +52,17 @@ import { defineConfig } from '@jrmc/adonis-mcp'
28
52
  export default defineConfig({
29
53
  name: 'adonis-mcp-server',
30
54
  version: '1.0.0',
31
- path: 'app/mcp', // Path where your tools will be stored
32
55
  })
33
56
  ```
34
57
 
58
+ By default, your MCP tools, resources, and prompts will be stored in `app/mcp`. If you want to use a different path, you need to configure it in your `adonisrc.ts` file:
59
+
60
+ ```typescript
61
+ directories: {
62
+ mcp: 'app/custom/mcp', // Optional: custom path for MCP files (defaults to 'app/mcp')
63
+ }
64
+ ```
65
+
35
66
  ## Usage
36
67
 
37
68
  ### Creating a Tool
@@ -45,8 +76,8 @@ node ace make:mcp-tool my_tool
45
76
  This command will create a file in `app/mcp/tools/my_tool.ts` with a base template:
46
77
 
47
78
  ```typescript
48
- import type { McpContext } from '@jrmc/adonis-mcp/types/context'
49
- import type { BaseSchema, InferJSONSchema } from '@jrmc/adonis-mcp/types/method'
79
+ import type { ToolContext } from '@jrmc/adonis-mcp/types/context'
80
+ import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
50
81
 
51
82
  import { Tool } from '@jrmc/adonis-mcp'
52
83
 
@@ -54,15 +85,13 @@ type Schema = BaseSchema<{
54
85
  text: { type: "string" }
55
86
  }>
56
87
 
57
- type Context = McpContext & { args: InferJSONSchema<Schema> }
58
-
59
- export default class MyToolTool implements Tool<Schema> {
88
+ export default class MyToolTool extends Tool<Schema> {
60
89
  name = 'tool_name'
61
90
  title = 'Tool title'
62
91
  description = 'Tool description'
63
92
 
64
- async handle({ args }: Context) {
65
- console.log(args.text)
93
+ async handle({ args, response }: ToolContext<Schema>) {
94
+ return response.text('Hello, world!')
66
95
  }
67
96
 
68
97
  schema() {
@@ -121,12 +150,33 @@ schema() {
121
150
  }
122
151
  ```
123
152
 
153
+ You can also use VineJS(>= [v4](https://vinejs.dev/docs/json-schema-generation)) to define your schema:
154
+
155
+ ```typescript
156
+ import vine from '@vinejs/vine'
157
+
158
+ const vineSchema = vine.object({
159
+ page: vine.number().meta({
160
+ description: 'page number for pagination',
161
+ }),
162
+ perPage: vine.number().optional().meta({
163
+ description: 'per page limit for pagination',
164
+ })
165
+ })
166
+
167
+ schema() {
168
+ return vine.create(
169
+ vineSchema
170
+ ).toJSONSchema() as Schema
171
+ }
172
+ ```
173
+
124
174
  ### Handler Implementation
125
175
 
126
176
  The `handle` method contains your tool's logic. It receives a typed context with validated arguments:
127
177
 
128
178
  ```typescript
129
- async handle({ args, response, auth, bouncer }: Context) {
179
+ async handle({ args, response, auth, bouncer }: ToolContext<Schema>) {
130
180
  // Your logic here
131
181
  const result = await SomeModel.query().where('id', args.id)
132
182
 
@@ -136,15 +186,15 @@ async handle({ args, response, auth, bouncer }: Context) {
136
186
 
137
187
  ### Setting up Authentication and Bouncer
138
188
 
139
- To use `auth` and `bouncer` in your MCP tools, add the following TypeScript declaration in your middleware (e.g., in your Bouncer initialization middleware):
189
+ To use `auth` and `bouncer` in your MCP tools, prompts, and resources, add the following TypeScript declaration in your middleware (e.g., in your MCP middleware):
140
190
 
141
191
  ```typescript
142
192
  declare module '@jrmc/adonis-mcp/types/context' {
143
193
  export interface McpContext {
144
- auth?: {
194
+ auth: {
145
195
  user?: HttpContext['auth']['user']
146
196
  }
147
- bouncer?: Bouncer<
197
+ bouncer: McpBouncer<
148
198
  Exclude<HttpContext['auth']['user'], undefined>,
149
199
  typeof abilities,
150
200
  typeof policies
@@ -173,12 +223,29 @@ You can also specify a custom path:
173
223
  router.mcp('/custom-mcp-path').use(middleware.auth())
174
224
  ```
175
225
 
226
+ > **⚠️ Important: CSRF Protection**
227
+ >
228
+ > If you have CSRF protection enabled in your application, you **must** exclude the MCP route from CSRF validation. MCP clients typically don't include CSRF tokens in their requests.
229
+ >
230
+ > In your `config/shield.ts` file, add the MCP route to the CSRF exceptions:
231
+ >
232
+ > ```typescript
233
+ > export const shieldConfig = defineConfig({
234
+ > csrf: {
235
+ > enabled: true,
236
+ > exceptRoutes: [
237
+ > '/mcp', // Or your custom MCP path
238
+ > ],
239
+ > },
240
+ > })
241
+ > ```
242
+
176
243
  ### Using Authentication
177
244
 
178
245
  The MCP context automatically includes the `auth` instance from the `HttpContext` if available. You can use it to access the authenticated user:
179
246
 
180
247
  ```typescript
181
- async handle({ args, auth }: Context) {
248
+ async handle({ args, auth, response }: ToolContext<Schema>) {
182
249
  const user = auth?.user
183
250
 
184
251
  if (!user) {
@@ -200,7 +267,7 @@ async handle({ args, auth }: Context) {
200
267
  The MCP context automatically includes the `bouncer` instance from the `HttpContext` if available. You can use it to check permissions:
201
268
 
202
269
  ```typescript
203
- async handle({ args, bouncer }: Context) {
270
+ async handle({ args, bouncer, response }: ToolContext<Schema>) {
204
271
  // Check a permission
205
272
  await bouncer.authorize('viewUsers')
206
273
 
@@ -214,13 +281,55 @@ async handle({ args, bouncer }: Context) {
214
281
 
215
282
  ### Response Return
216
283
 
217
- The context includes a `response` instance to format your responses. The most common method is `text()`:
284
+ The context includes a `response` instance to format your responses. The available methods depend on the context type:
285
+
286
+ #### Tool Responses
287
+
288
+ For tools, you can use:
289
+
290
+ - `response.text(text: string)`: Return plain text content
291
+ - `response.image(data: string, mimeType: string)`: Return image content (base64 encoded)
292
+ - `response.audio(data: string, mimeType: string)`: Return audio content (base64 encoded)
293
+ - `response.structured(object: Record<string, unknown>)`: Return structured JSON data
294
+ - `response.resourceLink(uri: string)`: Return a link to a resource
295
+ - `response.error(message: string)`: Return an error message
296
+ - `response.send(content: Content | Content[])`: Send custom content objects
218
297
 
219
298
  ```typescript
220
- async handle({ args, response }: Context) {
221
- const data = { success: true, message: 'Operation completed' }
299
+ async handle({ args, response }: ToolContext<Schema>) {
300
+ // Return text
301
+ return response.text(JSON.stringify({ success: true }))
302
+
303
+ // Return structured data
304
+ return response.structured({
305
+ temperature: 22.5,
306
+ conditions: 'Partly cloudy',
307
+ humidity: 65
308
+ })
309
+
310
+ // Return image
311
+ const imageData = await fs.readFile('path/to/image.png', 'base64')
312
+ return response.image(imageData, 'image/png')
313
+
314
+ // Return a resource link
315
+ return response.resourceLink('file:///path/to/resource.txt')
222
316
 
223
- return response.text(JSON.stringify(data))
317
+ // Return error
318
+ return response.error('Something went wrong')
319
+ }
320
+ ```
321
+
322
+ #### Resource Responses
323
+
324
+ For resources, you can use:
325
+
326
+ - `response.text(text: string)`: Return text content
327
+ - `response.blob(text: string)`: Return binary content (base64 encoded)
328
+
329
+ ```typescript
330
+ async handle({ response }: ResourceContext) {
331
+ const content = await fs.readFile('path/to/file.txt', 'utf-8')
332
+ return response.text(content)
224
333
  }
225
334
  ```
226
335
 
@@ -229,8 +338,8 @@ async handle({ args, response }: Context) {
229
338
  Here is a complete example of a tool that creates a bookmark:
230
339
 
231
340
  ```typescript
232
- import type { McpContext } from '@jrmc/adonis-mcp/types/context'
233
- import type { BaseSchema, InferJSONSchema } from '@jrmc/adonis-mcp/types/method'
341
+ import type { ToolContext } from '@jrmc/adonis-mcp/types/context'
342
+ import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
234
343
 
235
344
  import { Tool } from '@jrmc/adonis-mcp'
236
345
  import Bookmark from '#models/bookmark'
@@ -240,14 +349,12 @@ type Schema = BaseSchema<{
240
349
  url: { type: "string" }
241
350
  }>
242
351
 
243
- type Context = McpContext & { args: InferJSONSchema<Schema> }
244
-
245
- export default class AddBookmarkTool implements Tool<Schema> {
352
+ export default class AddBookmarkTool extends Tool<Schema> {
246
353
  name = 'create_bookmark'
247
354
  title = 'Create Bookmark'
248
355
  description = 'Create a new bookmark'
249
356
 
250
- async handle({ args, response, auth }: Context) {
357
+ async handle({ args, response, auth }: ToolContext<Schema>) {
251
358
  const bookmark = await Bookmark.create({
252
359
  title: args.title,
253
360
  text: args.url,
@@ -276,9 +383,708 @@ export default class AddBookmarkTool implements Tool<Schema> {
276
383
  }
277
384
  ```
278
385
 
386
+ ## Advanced Features
387
+
388
+ ### Structured Output
389
+
390
+ The `response.structured()` method allows you to return JSON data in a structured format. This is particularly useful when you want to return data that can be easily parsed and used by the MCP client without additional processing:
391
+
392
+ ```typescript
393
+ import type { ToolContext } from '@jrmc/adonis-mcp/types/context'
394
+ import { Tool } from '@jrmc/adonis-mcp'
395
+
396
+ export default class GetWeatherTool extends Tool {
397
+ name = 'get_weather'
398
+ title = 'Get Weather'
399
+ description = 'Get current weather data'
400
+
401
+ async handle({ args, response }: ToolContext) {
402
+ const weatherData = {
403
+ temperature: 22.5,
404
+ conditions: 'Partly cloudy',
405
+ humidity: 65,
406
+ windSpeed: 12,
407
+ location: args.location
408
+ }
409
+
410
+ return response.structured(weatherData)
411
+ }
412
+ }
413
+ ```
414
+
415
+ **Note:** Structured content can only be used in tools, not in prompts or resources.
416
+
417
+ ### Resource Links
418
+
419
+ Resource links allow you to reference other resources in your tool responses. This is useful when you want to point to additional information without embedding the entire resource content:
420
+
421
+ ```typescript
422
+ import type { ToolContext } from '@jrmc/adonis-mcp/types/context'
423
+ import { Tool } from '@jrmc/adonis-mcp'
424
+
425
+ export default class GetDocumentationTool extends Tool {
426
+ name = 'get_documentation'
427
+ title = 'Get Documentation'
428
+ description = 'Get documentation for a specific topic'
429
+
430
+ async handle({ args, response }: ToolContext) {
431
+ return [
432
+ response.text('Here is the documentation for your topic:'),
433
+ response.resourceLink(`file:///docs/${args.topic}.md`)
434
+ ]
435
+ }
436
+ }
437
+ ```
438
+
439
+ The resource link will include metadata about the resource (name, mimeType, title, description, size) without fetching the actual content. The MCP client can then decide whether to fetch the resource content separately.
440
+
441
+ **Note:** Resource links can only be used in tools, not in prompts or resources.
442
+
443
+ ### Metadata with `withMeta()`
444
+
445
+ The `withMeta()` method is available on all content types and allows you to attach custom metadata to your responses. This metadata can be used by MCP clients for various purposes such as logging, analytics, or custom processing:
446
+
447
+ ```typescript
448
+ async handle({ args, response }: ToolContext<Schema>) {
449
+ const users = await User.all()
450
+
451
+ return response.text(JSON.stringify(users)).withMeta({
452
+ source: 'database',
453
+ queryTime: Date.now(),
454
+ count: users.length,
455
+ cacheHit: false
456
+ })
457
+ }
458
+ ```
459
+
460
+ Metadata is particularly useful when:
461
+ - You want to provide debugging information
462
+ - You need to track the source of data
463
+ - You want to include performance metrics
464
+ - You need to pass additional context to the client
465
+
466
+ ### Annotations
467
+
468
+ Annotations allow you to provide additional metadata about your tools and resources to help MCP clients better understand their behavior and characteristics.
469
+
470
+ #### Tool Annotations
471
+
472
+ Tools support the following annotations that describe their operational characteristics:
473
+
474
+ ##### `@isReadOnly()`
475
+
476
+ Indicates that a tool only reads data and does not modify any state:
477
+
478
+ ```typescript
479
+ import { Tool } from '@jrmc/adonis-mcp'
480
+ import { isReadOnly } from '@jrmc/adonis-mcp/tool_annotations'
481
+
482
+ @isReadOnly()
483
+ export default class GetUserTool extends Tool {
484
+ name = 'get_user'
485
+
486
+ async handle({ args, response }: ToolContext) {
487
+ const user = await User.find(args.id)
488
+ return response.text(JSON.stringify(user))
489
+ }
490
+ }
491
+ ```
492
+
493
+ You can also explicitly set it to false:
494
+
495
+ ```typescript
496
+ @isReadOnly(false)
497
+ export default class UpdateUserTool extends Tool {
498
+ // ...
499
+ }
500
+ ```
501
+
502
+ ##### `@isOpenWorld()`
503
+
504
+ Indicates that a tool can access information from the internet or external sources:
505
+
506
+ ```typescript
507
+ import { isOpenWorld } from '@jrmc/adonis-mcp/tool_annotations'
508
+
509
+ @isOpenWorld()
510
+ export default class FetchWeatherTool extends Tool {
511
+ name = 'fetch_weather'
512
+
513
+ async handle({ args, response }: ToolContext) {
514
+ const weather = await externalApi.getWeather(args.city)
515
+ return response.text(JSON.stringify(weather))
516
+ }
517
+ }
518
+ ```
519
+
520
+ ##### `@isDestructive()`
521
+
522
+ Indicates that a tool performs destructive operations like deleting data:
523
+
524
+ ```typescript
525
+ import { isDestructive } from '@jrmc/adonis-mcp/tool_annotations'
526
+
527
+ @isDestructive()
528
+ export default class DeleteUserTool extends Tool {
529
+ name = 'delete_user'
530
+
531
+ async handle({ args, response }: ToolContext) {
532
+ await User.query().where('id', args.id).delete()
533
+ return response.text('User deleted successfully')
534
+ }
535
+ }
536
+ ```
537
+
538
+ ##### `@isIdempotent()`
539
+
540
+ Indicates that a tool can be safely called multiple times with the same arguments without causing different effects:
541
+
542
+ ```typescript
543
+ import { isIdempotent } from '@jrmc/adonis-mcp/tool_annotations'
544
+
545
+ @isIdempotent()
546
+ export default class SetUserStatusTool extends Tool {
547
+ name = 'set_user_status'
548
+
549
+ async handle({ args, response }: ToolContext) {
550
+ await User.query().where('id', args.id).update({ status: args.status })
551
+ return response.text('Status updated')
552
+ }
553
+ }
554
+ ```
555
+
556
+ ##### Combining Multiple Annotations
557
+
558
+ You can use multiple annotations on the same tool:
559
+
560
+ ```typescript
561
+ import { isReadOnly, isOpenWorld, isIdempotent } from '@jrmc/adonis-mcp/tool_annotations'
562
+
563
+ @isReadOnly()
564
+ @isOpenWorld()
565
+ @isIdempotent()
566
+ export default class SearchOnlineTool extends Tool {
567
+ name = 'search_online'
568
+
569
+ async handle({ args, response }: ToolContext) {
570
+ const results = await searchEngine.search(args.query)
571
+ return response.text(JSON.stringify(results))
572
+ }
573
+ }
574
+ ```
575
+
576
+ #### Resource Annotations
577
+
578
+ Resources support the following annotations to provide additional context:
579
+
580
+ ##### `@priority()`
581
+
582
+ Specifies the importance of a resource as a number between 0.0 and 1.0:
583
+
584
+ ```typescript
585
+ import { Resource } from '@jrmc/adonis-mcp'
586
+ import { priority } from '@jrmc/adonis-mcp/annotations'
587
+
588
+ @priority(0.9)
589
+ export default class ImportantDocResource extends Resource {
590
+ name = 'important_doc.txt'
591
+ uri = 'file:///important_doc.txt'
592
+
593
+ async handle({ response }: ResourceContext) {
594
+ return response.text('Critical documentation content')
595
+ }
596
+ }
597
+ ```
598
+
599
+ ##### `@audience()`
600
+
601
+ Specifies the intended audience for a resource (user, assistant, or both):
602
+
603
+ ```typescript
604
+ import { Resource } from '@jrmc/adonis-mcp'
605
+ import { audience } from '@jrmc/adonis-mcp/annotations'
606
+ import Role from '@jrmc/adonis-mcp/enums/role'
607
+
608
+ @audience(Role.USER)
609
+ export default class UserManualResource extends Resource {
610
+ name = 'user_manual.txt'
611
+ uri = 'file:///user_manual.txt'
612
+
613
+ async handle({ response }: ResourceContext) {
614
+ return response.text('User manual content')
615
+ }
616
+ }
617
+ ```
618
+
619
+ You can also specify multiple audiences:
620
+
621
+ ```typescript
622
+ @audience([Role.USER, Role.ASSISTANT])
623
+ export default class SharedDocResource extends Resource {
624
+ // ...
625
+ }
626
+ ```
627
+
628
+ ##### `@lastModified()`
629
+
630
+ Indicates when a resource was last updated (ISO 8601 timestamp):
631
+
632
+ ```typescript
633
+ import { Resource } from '@jrmc/adonis-mcp'
634
+ import { lastModified } from '@jrmc/adonis-mcp/annotations'
635
+
636
+ @lastModified('2024-12-12T10:00:00Z')
637
+ export default class DocumentResource extends Resource {
638
+ name = 'document.txt'
639
+ uri = 'file:///document.txt'
640
+
641
+ async handle({ response }: ResourceContext) {
642
+ return response.text('Document content')
643
+ }
644
+ }
645
+ ```
646
+
647
+ ##### Combining Resource Annotations
648
+
649
+ You can use multiple annotations on the same resource:
650
+
651
+ ```typescript
652
+ import { Resource } from '@jrmc/adonis-mcp'
653
+ import { priority, audience, lastModified } from '@jrmc/adonis-mcp/annotations'
654
+ import Role from '@jrmc/adonis-mcp/enums/role'
655
+
656
+ @priority(0.8)
657
+ @audience([Role.USER, Role.ASSISTANT])
658
+ @lastModified('2024-12-12T10:00:00Z')
659
+ export default class ApiDocResource extends Resource {
660
+ name = 'api_docs.txt'
661
+ uri = 'file:///api_docs.txt'
662
+
663
+ async handle({ response }: ResourceContext) {
664
+ return response.text('API documentation content')
665
+ }
666
+ }
667
+ ```
668
+
669
+ ### Creating a Resource
670
+
671
+ To create a new resource, use the Ace command:
672
+
673
+ ```bash
674
+ node ace make:mcp-resource my_resource
675
+ ```
676
+
677
+ This command will create a file in `app/mcp/resources/my_resource.ts` with a base template:
678
+
679
+ ```typescript
680
+ import type { ResourceContext } from '@jrmc/adonis-mcp/types/context'
681
+
682
+ import { Resource } from '@jrmc/adonis-mcp'
683
+
684
+ export default class MyResourceResource extends Resource {
685
+ name = 'example.txt'
686
+ uri = 'file:///example.txt'
687
+ mimeType = 'text/plain'
688
+ title = 'Resource title'
689
+ description = 'Resource description'
690
+ size = 0
691
+
692
+ async handle({ response }: ResourceContext) {
693
+ this.size = 1000
694
+ return response.text('Hello World')
695
+ }
696
+ }
697
+ ```
698
+
699
+ ### Resource Properties
700
+
701
+ Resources have the following properties:
702
+
703
+ - `name` (optional): The name of the resource
704
+ - `uri` (required): The unique identifier for the resource (must be unique)
705
+ - `mimeType` (optional): The MIME type of the resource
706
+ - `title` (optional): A human-readable title
707
+ - `description` (optional): A description of the resource
708
+ - `size` (optional): The size of the resource in bytes
709
+
710
+ ### Resource Handler
711
+
712
+ The `handle` method returns the content of the resource. You can use `response.text()` for text content or `response.blob()` for binary content:
713
+
714
+ ```typescript
715
+ async handle({ response }: ResourceContext) {
716
+ const content = await fs.readFile('path/to/file.txt', 'utf-8')
717
+ this.size = content.length
718
+ return response.text(content)
719
+ }
720
+ ```
721
+
722
+ ### URI Templates
723
+
724
+ Resources support URI templates (RFC 6570) to create dynamic resources. This allows you to define resources with variable parts in their URIs:
725
+
726
+ ```typescript
727
+ import type { ResourceContext } from '@jrmc/adonis-mcp/types/context'
728
+ import { Resource } from '@jrmc/adonis-mcp'
729
+
730
+ type Args = {
731
+ name: string
732
+ }
733
+
734
+ export default class RobotsResource extends Resource<Args> {
735
+ name = 'robots.txt'
736
+ uri = 'file:///{name}.txt'
737
+ mimeType = 'text/plain'
738
+ title = 'Robots file'
739
+ description = 'Dynamic robots.txt file'
740
+
741
+ async handle({ args, response }: ResourceContext<Args>) {
742
+ this.size = 1000
743
+ return response.text(`Hello World ${args?.name}`)
744
+ }
745
+ }
746
+ ```
747
+
748
+ When a client requests `file:///robots.txt`, the template `file:///{name}.txt` will match and extract `name: "robots"` as an argument, which will be available in the `handle` method via `args.name`.
749
+
750
+ URI templates support various operators:
751
+ - `{name}` - Simple variable substitution
752
+ - `{/name}` - Path segment
753
+ - `{?name}` - Query parameter
754
+ - `{&name}` - Additional query parameter
755
+ - `{#name}` - Fragment identifier
756
+ - `{+name}` - Reserved characters allowed
757
+ - `{.name}` - Dot-prefixed segment
758
+
759
+ For more information, see [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570).
760
+
761
+ ### Creating a Prompt
762
+
763
+ To create a new prompt, use the Ace command:
764
+
765
+ ```bash
766
+ node ace make:mcp-prompt my_prompt
767
+ ```
768
+
769
+ This command will create a file in `app/mcp/prompts/my_prompt.ts` with a base template:
770
+
771
+ ```typescript
772
+ import type { PromptContext } from '@jrmc/adonis-mcp/types/context'
773
+ import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
774
+
775
+ import { Prompt } from '@jrmc/adonis-mcp'
776
+
777
+ type Schema = BaseSchema<{
778
+ text: { type: "string" }
779
+ }>
780
+
781
+ export default class MyPromptPrompt extends Prompt<Schema> {
782
+ name = 'my_prompt'
783
+ title = 'Prompt title'
784
+ description = 'Prompt description'
785
+
786
+ async handle({ args, response }: PromptContext<Schema>) {
787
+ return [
788
+ response.text('Hello, world!')
789
+ ]
790
+ }
791
+
792
+ schema() {
793
+ return {
794
+ type: "object",
795
+ properties: {
796
+ text: {
797
+ type: "string",
798
+ description: "Description text argument"
799
+ },
800
+ },
801
+ required: ["text"]
802
+ } as Schema
803
+ }
804
+ }
805
+ ```
806
+
807
+ ### Prompt Schema
808
+
809
+ Prompts use the same schema definition system as tools, following the [JSON Schema](https://json-schema.org/) specification. You can also use Zod to define your schema:
810
+
811
+ ```typescript
812
+ import * as z from 'zod'
813
+
814
+ const zodSchema = z.object({
815
+ code: z.string(),
816
+ language: z.string().optional()
817
+ })
818
+
819
+ schema() {
820
+ return z.toJSONSchema(
821
+ zodSchema,
822
+ { io: "input" }
823
+ ) as Schema
824
+ }
825
+ ```
826
+
827
+ ### Prompt Handler
828
+
829
+ The `handle` method for prompts returns an array of content objects. This allows you to return multiple pieces of content, including embedded resources:
830
+
831
+ ```typescript
832
+ async handle({ args, response }: PromptContext<Schema>) {
833
+ return [
834
+ response.text(`Please review this code:\n\n${args.code}`),
835
+ response.embeddedResource('file:///example.txt')
836
+ ]
837
+ }
838
+ ```
839
+
840
+ ### Prompt Response Methods
841
+
842
+ For prompts, you can use the same response methods as tools, but you must return an array:
843
+
844
+ - `response.text(text: string)`: Return plain text content
845
+ - `response.image(data: string, mimeType: string)`: Return image content (base64 encoded)
846
+ - `response.audio(data: string, mimeType: string)`: Return audio content (base64 encoded)
847
+ - `response.embeddedResource(uri: string)`: Embed another resource in the prompt response
848
+
849
+ All response methods also support the `withMeta()` method to add metadata:
850
+
851
+ ```typescript
852
+ async handle({ args, response }: PromptContext<Schema>) {
853
+ return [
854
+ response.text('Here is the code to review:').withMeta({
855
+ language: args.language
856
+ }),
857
+ response.embeddedResource('file:///code.py'),
858
+ response.text('Please provide feedback.')
859
+ ]
860
+ }
861
+ ```
862
+
863
+ ### Complete Prompt Example
864
+
865
+ Here is a complete example of a prompt for code review:
866
+
867
+ ```typescript
868
+ import type { PromptContext } from '@jrmc/adonis-mcp/types/context'
869
+ import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
870
+
871
+ import { Prompt } from '@jrmc/adonis-mcp'
872
+
873
+ type Schema = BaseSchema<{
874
+ code: { type: "string" }
875
+ language: { type: "string" }
876
+ }>
877
+
878
+ export default class CodeReviewPrompt extends Prompt<Schema> {
879
+ name = 'code_review'
880
+ title = 'Code Review'
881
+ description = 'Review code and provide feedback'
882
+
883
+ async handle({ args, response }: PromptContext<Schema>) {
884
+ return [
885
+ response.text(`Please review this ${args.language} code:\n\n${args.code}`),
886
+ response.text('Provide feedback on code quality, potential bugs, and improvements.')
887
+ ]
888
+ }
889
+
890
+ schema() {
891
+ return {
892
+ type: "object",
893
+ properties: {
894
+ code: {
895
+ type: "string",
896
+ description: "The code to review"
897
+ },
898
+ language: {
899
+ type: "string",
900
+ description: "Programming language"
901
+ }
902
+ },
903
+ required: ["code", "language"]
904
+ } as Schema
905
+ }
906
+ }
907
+ ```
908
+
909
+ ## Completions
910
+
911
+ Completions provide argument suggestions for prompts and resources, helping users fill in parameters interactively. This feature must be enabled in your configuration and can be implemented for both prompts and resources.
912
+
913
+ ### Enabling Completions
914
+
915
+ First, enable completions in your `config/mcp.ts`:
916
+
917
+ ```typescript
918
+ import { defineConfig } from '@jrmc/adonis-mcp'
919
+
920
+ export default defineConfig({
921
+ name: 'adonis-mcp-server',
922
+ version: '1.0.0',
923
+ completions: true, // Enable completions
924
+ })
925
+ ```
926
+
927
+ ### Implementing Completions in Prompts
928
+
929
+ Add a `complete()` method to your prompt to provide argument suggestions:
930
+
931
+ ```typescript
932
+ import type { PromptContext, CompleteContext } from '@jrmc/adonis-mcp/types/context'
933
+ import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
934
+
935
+ import { Prompt } from '@jrmc/adonis-mcp'
936
+
937
+ type Schema = BaseSchema<{
938
+ language: { type: "string" }
939
+ code: { type: "string" }
940
+ }>
941
+
942
+ export default class CodeReviewPrompt extends Prompt<Schema> {
943
+ name = 'code_review'
944
+ title = 'Code Review'
945
+ description = 'Review code and provide feedback'
946
+
947
+ async handle({ args, response }: PromptContext<Schema>) {
948
+ return [
949
+ response.text(`Please review this ${args.language} code:\n\n${args.code}`)
950
+ ]
951
+ }
952
+
953
+ async complete({ args, response }: CompleteContext<Schema>) {
954
+ // Provide language suggestions when the user types
955
+ if (args?.language !== undefined) {
956
+ return response.complete({
957
+ values: ['python', 'javascript', 'typescript', 'java', 'go', 'rust']
958
+ })
959
+ }
960
+
961
+ return response.complete({ values: [] })
962
+ }
963
+
964
+ schema() {
965
+ return {
966
+ type: "object",
967
+ properties: {
968
+ language: {
969
+ type: "string",
970
+ description: "Programming language"
971
+ },
972
+ code: {
973
+ type: "string",
974
+ description: "Code to review"
975
+ }
976
+ },
977
+ required: ["language", "code"]
978
+ } as Schema
979
+ }
980
+ }
981
+ ```
982
+
983
+ ### Implementing Completions in Resources
984
+
985
+ Resources with URI templates can also provide completions for their path parameters:
986
+
987
+ ```typescript
988
+ import type { ResourceContext, CompleteContext } from '@jrmc/adonis-mcp/types/context'
989
+ import { Resource } from '@jrmc/adonis-mcp'
990
+
991
+ type Args = {
992
+ directory: string
993
+ name: string
994
+ }
995
+
996
+ export default class ConfigFileResource extends Resource<Args> {
997
+ name = 'config_file'
998
+ uri = 'file://{directory}/{name}.txt'
999
+ mimeType = 'text/plain'
1000
+ title = 'Configuration File'
1001
+ description = 'Access configuration files'
1002
+
1003
+ async handle({ args, response }: ResourceContext<Args>) {
1004
+ const content = await readConfigFile(args.directory, args.name)
1005
+ return response.text(content)
1006
+ }
1007
+
1008
+ async complete({ args, response }: CompleteContext<Args>) {
1009
+ // Provide suggestions based on available directories and files
1010
+ if (args?.name !== undefined) {
1011
+ return response.complete({
1012
+ values: ['config', 'settings', 'environment', 'database']
1013
+ })
1014
+ }
1015
+
1016
+ if (args?.directory !== undefined) {
1017
+ return response.complete({
1018
+ values: ['production', 'staging', 'development']
1019
+ })
1020
+ }
1021
+
1022
+ return response.complete({ values: [] })
1023
+ }
1024
+ }
1025
+ ```
1026
+
1027
+ ### Completion Context
1028
+
1029
+ The `complete()` method receives a `CompleteContext` that includes:
1030
+
1031
+ - `args`: The current argument values (partial or complete)
1032
+ - `response`: The response object with a `complete()` method
1033
+
1034
+ The response format includes:
1035
+
1036
+ ```typescript
1037
+ response.complete({
1038
+ values: string[], // Array of suggested values
1039
+ hasMore?: boolean, // Optional: indicates if more values are available
1040
+ total?: number // Optional: total number of available values
1041
+ })
1042
+ ```
1043
+
1044
+ ### Transports
1045
+
1046
+ The package supports multiple transport mechanisms:
1047
+
1048
+ - **HTTP Transport**: Default transport for HTTP-based MCP servers (used when accessing via HTTP routes)
1049
+ - **Stdio Transport**: For command-line MCP servers that communicate via standard input/output
1050
+ - **Fake Transport**: For testing purposes, allows you to capture and inspect MCP messages
1051
+
279
1052
  ### Pagination
280
1053
 
281
- The `tools/list` method supports cursor-based pagination to handle large numbers of tools efficiently. This is particularly useful when you have many tools registered in your application. [More information](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/pagination)
1054
+ The `tools/list` and `resources/list` methods support cursor-based pagination to handle large numbers of tools and resources efficiently. This is particularly useful when you have many tools or resources registered in your application. [More information](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/pagination)
1055
+
1056
+ ## Testing & Debugging
1057
+
1058
+ ### MCP Inspector
1059
+
1060
+ The MCP Inspector is a powerful tool for debugging and testing your MCP server. It provides a graphical interface to interact with your tools, resources, and prompts.
1061
+
1062
+ To open the MCP Inspector, use the following command:
1063
+
1064
+ ```bash
1065
+ node ace mcp:inspector
1066
+ ```
1067
+
1068
+ By default, this command uses HTTP transport. You can specify a different transport type:
1069
+
1070
+ ```bash
1071
+ # Use HTTP transport (default)
1072
+ node ace mcp:inspector http
1073
+
1074
+ # Use stdio transport
1075
+ node ace mcp:inspector stdio
1076
+ ```
1077
+
1078
+ **Important notes:**
1079
+
1080
+ - The inspector can only be used in development environment (not in production)
1081
+ - For HTTP transport, make sure your server is running and the MCP route is configured
1082
+ - The inspector will automatically connect to your MCP server and allow you to:
1083
+ - List and test all available tools
1084
+ - Browse and read resources
1085
+ - Execute prompts with different arguments
1086
+ - Inspect request/response payloads
1087
+ - Debug any issues with your MCP implementation
282
1088
 
283
1089
  ## Support
284
1090