@@ -94,19 +94,10 @@ export class McpPostHookProvider implements Provider<Function> {
9494Update your ` src/application.ts ` to bind the component and hooks:
9595
9696``` typescript
97- import {BootMixin , ServiceMixin } from ' @loopback/core' ;
98- import {RepositoryMixin , RestApplication } from ' @loopback/rest' ;
99- import {McpComponent } from ' loopback4-mcp' ;
100- import {McpHookBindings } from ' ./keys' ;
101- import {McpPreHookProvider } from ' ./providers/mcp-pre-hook.provider' ;
102- import {McpPostHookProvider } from ' ./providers/mcp-post-hook.provider' ;
103-
10497export class MyApplication extends BootMixin (
10598 ServiceMixin (RepositoryMixin (RestApplication )),
10699) {
107100 constructor (options : ApplicationConfig = {}) {
108- super (options );
109-
110101 // Bind MCP component
111102 this .component (McpComponent );
112103
@@ -122,26 +113,6 @@ export class MyApplication extends BootMixin(
122113Add the ` @mcpTool() ` decorator to controller methods you want to expose as MCP tools. Here's a complete example showing the decorator stack with authorization and authentication:
123114
124115``` typescript
125- import {
126- Count ,
127- CountSchema ,
128- Filter ,
129- repository ,
130- Where ,
131- } from ' @loopback/repository' ;
132- import {param , post , get , patch , put , del , requestBody } from ' @loopback/rest' ;
133- import {authorize } from ' loopback4-authorization' ;
134- import {authenticate , STRATEGY } from ' loopback4-authentication' ;
135- import {PermissionKey } from ' ../permissions' ;
136- import {
137- OPERATION_SECURITY_SPEC ,
138- STATUS_CODE ,
139- getModelSchemaRefSF ,
140- } from ' @sourceloop/core' ;
141- import {mcpTool } from ' loopback4-mcp' ;
142- import {HookBindings } from ' ../keys' ;
143- import {User , UserRepository } from ' ../models' ;
144-
145116export class UserController {
146117 constructor (
147118 @repository (UserRepository )
@@ -183,209 +154,6 @@ export class UserController {
183154 }]
184155 };
185156 }
186-
187- @authorize ({
188- permissions: [PermissionKey .ViewUser ],
189- })
190- @authenticate (STRATEGY .BEARER , {
191- passReqToCallback: true ,
192- })
193- @mcpTool ({
194- name: ' getUserById' ,
195- description: ' Get a user by ID' ,
196- preHook: {binding: HookBindings .PRE_HOOK },
197- postHook: {binding: HookBindings .POST_HOOK },
198- })
199- @get (' /users/{id}' , {
200- security: OPERATION_SECURITY_SPEC ,
201- responses: {
202- [STATUS_CODE .OK ]: {
203- description: ' User model instance' ,
204- content: {
205- ' application/json' : {
206- schema: getModelSchemaRefSF (User , {includeRelations: true }),
207- },
208- },
209- },
210- },
211- })
212- async findById(
213- @param .path .string (' id' ) id : string ,
214- ): Promise <User > {
215- return this .userRepository .findById (id );
216- }
217-
218- @authorize ({
219- permissions: [PermissionKey .ViewUser ],
220- })
221- @authenticate (STRATEGY .BEARER , {
222- passReqToCallback: true ,
223- })
224- @mcpTool ({
225- name: ' listUsers' ,
226- description: ' List all users' ,
227- preHook: {binding: HookBindings .PRE_HOOK },
228- postHook: {binding: HookBindings .POST_HOOK },
229- })
230- @get (' /users' , {
231- security: OPERATION_SECURITY_SPEC ,
232- responses: {
233- [STATUS_CODE .OK ]: {
234- description: ' Array of User model instances' ,
235- content: {
236- ' application/json' : {
237- schema: {
238- type: ' array' ,
239- items: getModelSchemaRefSF (User , {includeRelations: true }),
240- },
241- },
242- },
243- },
244- },
245- })
246- async find(
247- @param .filter (User ) filter ? : Filter <User >,
248- ): Promise <User []> {
249- return this .userRepository .find (filter );
250- }
251-
252- @authorize ({
253- permissions: [PermissionKey .UpdateUser ],
254- })
255- @authenticate (STRATEGY .BEARER , {
256- passReqToCallback: true ,
257- })
258- @mcpTool ({
259- name: ' updateUserById' ,
260- description: ' Update a user by ID' ,
261- preHook: {binding: HookBindings .PRE_HOOK },
262- postHook: {binding: HookBindings .POST_HOOK },
263- })
264- @patch (' /users/{id}' , {
265- security: OPERATION_SECURITY_SPEC ,
266- responses: {
267- [STATUS_CODE .NO_CONTENT ]: {
268- description: ' User PATCH success' ,
269- },
270- },
271- })
272- async updateById(
273- @param .path .string (' id' ) id : string ,
274- @param .query .object (' user' ) user : User ,
275- ): Promise <object > {
276- await this .userRepository .updateById (id , user );
277-
278- return {
279- content: [{
280- type: ' text' ,
281- text: ` Successfully updated user with id: ${id } `
282- }]
283- };
284- }
285-
286- @authorize ({
287- permissions: [PermissionKey .DeleteUser ],
288- })
289- @authenticate (STRATEGY .BEARER , {
290- passReqToCallback: true ,
291- })
292- @mcpTool ({
293- name: ' deleteUser' ,
294- description: ' Delete a user by ID' ,
295- preHook: {binding: HookBindings .PRE_HOOK },
296- postHook: {binding: HookBindings .POST_HOOK },
297- })
298- @del (' /users/{id}' , {
299- security: OPERATION_SECURITY_SPEC ,
300- responses: {
301- [STATUS_CODE .NO_CONTENT ]: {
302- description: ' User DELETE success' ,
303- },
304- },
305- })
306- async deleteById(@param .path .string (' id' ) id : string ): Promise <object > {
307- // Verify user exists first (will throw 404 if not found)
308- await this .userRepository .findById (id );
309-
310- await this .userRepository .deleteById (id );
311-
312- return {
313- content: [{
314- type: ' text' ,
315- text: ` Successfully deleted user with id: ${id } `
316- }]
317- };
318- }
319- }
320- ```
321-
322- ## Component Interaction Flow
323-
324- ** Request Flow:**
325- ```
326- Client Request (POST /mcp?tool=toolName)
327- ↓
328- McpController receives request and creates MCP server
329- ↓
330- McpServerFactory generates per-request server instance
331- ↓
332- McpToolRegistry looks up tool by name
333- ↓
334- Authorization Check validates JWT and permissions
335- ↓
336- Pre-Hook (if configured) performs validation/sanitization
337- ↓
338- Controller Method executes business logic
339- ↓
340- Post-Hook (if configured) performs logging/audit trails
341- ↓
342- Response Formatting wraps result in MCP format
343- ↓
344- Client receives MCP-formatted response
345- ```
346-
347- ** Error Flow:**
348- ```
349- Authorization Check Failed → 403 Forbidden Response → Client receives error
350- Controller Method Error → Error Formatting → Client receives MCP-formatted error
351- Hook Execution Error → Error Handling → Client receives MCP-formatted error
352- ```
353-
354- ## MCP Endpoint Usage
355-
356- ### Endpoint Format
357-
358- ** POST** ` /mcp?tool=toolName `
359-
360- ** Required Headers:**
361- - ` Content-Type: application/json `
362- - ` Authorization: Bearer YOUR_JWT_TOKEN `
363-
364- ### Example Request
365-
366- ``` bash
367- curl -X POST http://localhost:3000/mcp? tool=create-user \
368- -H " Content-Type: application/json" \
369- -H " Authorization: Bearer YOUR_JWT_TOKEN" \
370- -d ' {
371- "user": {
372- "email": "john@example.com",
373- "name": "John Doe",
374- "age": 30
375- }
376- }'
377- ```
378-
379- ### Example Response
380-
381- ``` json
382- {
383- "content" : [
384- {
385- "type" : " text" ,
386- "text" : " User created with ID: 550e8400-e29b-41d4-a716-446655440000"
387- }
388- ]
389157}
390158```
391159
@@ -474,95 +242,6 @@ npx @modelcontextprotocol/inspector http://localhost:3000/mcp
4742425 . Monitor hook execution and responses
4752436 . Debug issues using the detailed logs
476244
477- ## Troubleshooting
478-
479- ### Common Issues
480-
481- ** Parameter extraction failures**
482- - ** Cause:** Missing or incorrect ` @param ` decorators
483- - ** Solution:** Ensure all parameters have appropriate decorators based on route structure
484-
485- ** "Invalid tools/call result: expected object, received undefined"**
486- - ** Cause:** Method returns ` void ` instead of object
487- - ** Solution:** Always return explicit MCP-formatted response for non-read operations
488-
489- ** Hook not executing**
490- - ** Cause:** Hook not bound in application.ts or binding key mismatch
491- - ** Solution:** Verify hook providers are bound and binding keys match decorator configuration
492-
493- ** Authorization failures**
494- - ** Cause:** Missing ` @authorize() ` decorator or invalid JWT token
495- - ** Solution:** Add appropriate authorization decorators and ensure valid authentication
496-
497- ## Best Practices
498-
499- 1 . ** Always use ` @param ` decorators** - MCP tool will fail without them
500- 2 . ** Return MCP-formatted responses** for write operations (CREATE, UPDATE, DELETE)
501- 3 . ** Implement proper error handling** in controller methods and hooks
502- 4 . ** Use hooks for cross-cutting concerns** - validation, logging, audit trails
503- 5 . ** Test with curl first** before integrating with MCP clients
504- 6 . ** Monitor hook execution time** - hooks should be fast and non-blocking
505- 7 . ** Keep controller methods focused** - move complex logic to services
506- 8 . ** Use descriptive tool names** and detailed descriptions for better discovery
507-
508- ## Advanced Features
509-
510- ### Zod Schema Validation
511-
512- Add input validation using Zod schemas:
513-
514- ``` typescript
515- import {z } from ' zod' ;
516-
517- @mcpTool ({
518- name: ' create-user' ,
519- description: ' Create a new user' ,
520- schema: {
521- email: z .string ().email (' Invalid email format' ),
522- name: z .string ().min (2 , ' Name must be at least 2 characters' ),
523- age: z .number ().min (18 , ' User must be 18 or older' ),
524- },
525- })
526- ```
527-
528- ### Custom Hook Implementations
529-
530- Create more sophisticated hooks for business logic:
531-
532- ``` typescript
533- export class ValidationHookProvider implements Provider <Function > {
534- value(): Function {
535- return async (context : McpHookContext ) => {
536- if (context .toolName === ' create-user' ) {
537- const user = context .args .user as any ;
538-
539- // Validate email uniqueness
540- const existing = await this .userRepository .findOne ({
541- where: {email: user .email }
542- });
543-
544- if (existing ) {
545- throw new Error (' User with this email already exists' );
546- }
547-
548- // Sanitize input
549- user .name = user .name .trim ();
550- user .email = user .email .toLowerCase ();
551- }
552- };
553- }
554- }
555- ```
556-
557- ## Complete Example
558-
559- For a complete working example, refer to the test suite in ` src/__tests__/ ` which demonstrates:
560-
561- - Controller setup with ` @mcpTool ` decorators
562- - Hook provider implementations
563- - Integration with authorization system
564- - Request/response handling
565- - Error management
566245
567246## License
568247
@@ -572,3 +251,4 @@ For a complete working example, refer to the test suite in `src/__tests__/` whic
572251
573252- GitHub Issues: [ sourcefuse/loopback4-mcp/issues] ( https://github.com/sourcefuse/loopback4-mcp/issues )
574253- Documentation: [ loopback.io] ( https://loopback.io/ )
254+
0 commit comments