From 3dc4a9228b269638ebedb89a5195ceba2d47bdf1 Mon Sep 17 00:00:00 2001 From: Vinay Gupta Date: Tue, 21 Apr 2026 19:00:41 +0530 Subject: [PATCH] docs(core): readme update --- README.md | 322 +----------------------------------------------------- 1 file changed, 1 insertion(+), 321 deletions(-) diff --git a/README.md b/README.md index a1d39c1..ff461d4 100644 --- a/README.md +++ b/README.md @@ -94,19 +94,10 @@ export class McpPostHookProvider implements Provider { Update your `src/application.ts` to bind the component and hooks: ```typescript -import {BootMixin, ServiceMixin} from '@loopback/core'; -import {RepositoryMixin, RestApplication} from '@loopback/rest'; -import {McpComponent} from 'loopback4-mcp'; -import {McpHookBindings} from './keys'; -import {McpPreHookProvider} from './providers/mcp-pre-hook.provider'; -import {McpPostHookProvider} from './providers/mcp-post-hook.provider'; - export class MyApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { constructor(options: ApplicationConfig = {}) { - super(options); - // Bind MCP component this.component(McpComponent); @@ -122,26 +113,6 @@ export class MyApplication extends BootMixin( Add 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: ```typescript -import { - Count, - CountSchema, - Filter, - repository, - Where, -} from '@loopback/repository'; -import {param, post, get, patch, put, del, requestBody} from '@loopback/rest'; -import {authorize} from 'loopback4-authorization'; -import {authenticate, STRATEGY} from 'loopback4-authentication'; -import {PermissionKey} from '../permissions'; -import { - OPERATION_SECURITY_SPEC, - STATUS_CODE, - getModelSchemaRefSF, -} from '@sourceloop/core'; -import {mcpTool} from 'loopback4-mcp'; -import {HookBindings} from '../keys'; -import {User, UserRepository} from '../models'; - export class UserController { constructor( @repository(UserRepository) @@ -183,209 +154,6 @@ export class UserController { }] }; } - - @authorize({ - permissions: [PermissionKey.ViewUser], - }) - @authenticate(STRATEGY.BEARER, { - passReqToCallback: true, - }) - @mcpTool({ - name: 'getUserById', - description: 'Get a user by ID', - preHook: {binding: HookBindings.PRE_HOOK}, - postHook: {binding: HookBindings.POST_HOOK}, - }) - @get('/users/{id}', { - security: OPERATION_SECURITY_SPEC, - responses: { - [STATUS_CODE.OK]: { - description: 'User model instance', - content: { - 'application/json': { - schema: getModelSchemaRefSF(User, {includeRelations: true}), - }, - }, - }, - }, - }) - async findById( - @param.path.string('id') id: string, - ): Promise { - return this.userRepository.findById(id); - } - - @authorize({ - permissions: [PermissionKey.ViewUser], - }) - @authenticate(STRATEGY.BEARER, { - passReqToCallback: true, - }) - @mcpTool({ - name: 'listUsers', - description: 'List all users', - preHook: {binding: HookBindings.PRE_HOOK}, - postHook: {binding: HookBindings.POST_HOOK}, - }) - @get('/users', { - security: OPERATION_SECURITY_SPEC, - responses: { - [STATUS_CODE.OK]: { - description: 'Array of User model instances', - content: { - 'application/json': { - schema: { - type: 'array', - items: getModelSchemaRefSF(User, {includeRelations: true}), - }, - }, - }, - }, - }, - }) - async find( - @param.filter(User) filter?: Filter, - ): Promise { - return this.userRepository.find(filter); - } - - @authorize({ - permissions: [PermissionKey.UpdateUser], - }) - @authenticate(STRATEGY.BEARER, { - passReqToCallback: true, - }) - @mcpTool({ - name: 'updateUserById', - description: 'Update a user by ID', - preHook: {binding: HookBindings.PRE_HOOK}, - postHook: {binding: HookBindings.POST_HOOK}, - }) - @patch('/users/{id}', { - security: OPERATION_SECURITY_SPEC, - responses: { - [STATUS_CODE.NO_CONTENT]: { - description: 'User PATCH success', - }, - }, - }) - async updateById( - @param.path.string('id') id: string, - @param.query.object('user') user: User, - ): Promise { - await this.userRepository.updateById(id, user); - - return { - content: [{ - type: 'text', - text: `Successfully updated user with id: ${id}` - }] - }; - } - - @authorize({ - permissions: [PermissionKey.DeleteUser], - }) - @authenticate(STRATEGY.BEARER, { - passReqToCallback: true, - }) - @mcpTool({ - name: 'deleteUser', - description: 'Delete a user by ID', - preHook: {binding: HookBindings.PRE_HOOK}, - postHook: {binding: HookBindings.POST_HOOK}, - }) - @del('/users/{id}', { - security: OPERATION_SECURITY_SPEC, - responses: { - [STATUS_CODE.NO_CONTENT]: { - description: 'User DELETE success', - }, - }, - }) - async deleteById(@param.path.string('id') id: string): Promise { - // Verify user exists first (will throw 404 if not found) - await this.userRepository.findById(id); - - await this.userRepository.deleteById(id); - - return { - content: [{ - type: 'text', - text: `Successfully deleted user with id: ${id}` - }] - }; - } -} -``` - -## Component Interaction Flow - -**Request Flow:** -``` -Client Request (POST /mcp?tool=toolName) - ↓ -McpController receives request and creates MCP server - ↓ -McpServerFactory generates per-request server instance - ↓ -McpToolRegistry looks up tool by name - ↓ -Authorization Check validates JWT and permissions - ↓ -Pre-Hook (if configured) performs validation/sanitization - ↓ -Controller Method executes business logic - ↓ -Post-Hook (if configured) performs logging/audit trails - ↓ -Response Formatting wraps result in MCP format - ↓ -Client receives MCP-formatted response -``` - -**Error Flow:** -``` -Authorization Check Failed → 403 Forbidden Response → Client receives error -Controller Method Error → Error Formatting → Client receives MCP-formatted error -Hook Execution Error → Error Handling → Client receives MCP-formatted error -``` - -## MCP Endpoint Usage - -### Endpoint Format - -**POST** `/mcp?tool=toolName` - -**Required Headers:** -- `Content-Type: application/json` -- `Authorization: Bearer YOUR_JWT_TOKEN` - -### Example Request - -```bash -curl -X POST http://localhost:3000/mcp?tool=create-user \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -d '{ - "user": { - "email": "john@example.com", - "name": "John Doe", - "age": 30 - } - }' -``` - -### Example Response - -```json -{ - "content": [ - { - "type": "text", - "text": "User created with ID: 550e8400-e29b-41d4-a716-446655440000" - } - ] } ``` @@ -474,95 +242,6 @@ npx @modelcontextprotocol/inspector http://localhost:3000/mcp 5. Monitor hook execution and responses 6. Debug issues using the detailed logs -## Troubleshooting - -### Common Issues - -**Parameter extraction failures** -- **Cause:** Missing or incorrect `@param` decorators -- **Solution:** Ensure all parameters have appropriate decorators based on route structure - -**"Invalid tools/call result: expected object, received undefined"** -- **Cause:** Method returns `void` instead of object -- **Solution:** Always return explicit MCP-formatted response for non-read operations - -**Hook not executing** -- **Cause:** Hook not bound in application.ts or binding key mismatch -- **Solution:** Verify hook providers are bound and binding keys match decorator configuration - -**Authorization failures** -- **Cause:** Missing `@authorize()` decorator or invalid JWT token -- **Solution:** Add appropriate authorization decorators and ensure valid authentication - -## Best Practices - -1. **Always use `@param` decorators** - MCP tool will fail without them -2. **Return MCP-formatted responses** for write operations (CREATE, UPDATE, DELETE) -3. **Implement proper error handling** in controller methods and hooks -4. **Use hooks for cross-cutting concerns** - validation, logging, audit trails -5. **Test with curl first** before integrating with MCP clients -6. **Monitor hook execution time** - hooks should be fast and non-blocking -7. **Keep controller methods focused** - move complex logic to services -8. **Use descriptive tool names** and detailed descriptions for better discovery - -## Advanced Features - -### Zod Schema Validation - -Add input validation using Zod schemas: - -```typescript -import {z} from 'zod'; - -@mcpTool({ - name: 'create-user', - description: 'Create a new user', - schema: { - email: z.string().email('Invalid email format'), - name: z.string().min(2, 'Name must be at least 2 characters'), - age: z.number().min(18, 'User must be 18 or older'), - }, -}) -``` - -### Custom Hook Implementations - -Create more sophisticated hooks for business logic: - -```typescript -export class ValidationHookProvider implements Provider { - value(): Function { - return async (context: McpHookContext) => { - if (context.toolName === 'create-user') { - const user = context.args.user as any; - - // Validate email uniqueness - const existing = await this.userRepository.findOne({ - where: {email: user.email} - }); - - if (existing) { - throw new Error('User with this email already exists'); - } - - // Sanitize input - user.name = user.name.trim(); - user.email = user.email.toLowerCase(); - } - }; - } -} -``` - -## Complete Example - -For a complete working example, refer to the test suite in `src/__tests__/` which demonstrates: - -- Controller setup with `@mcpTool` decorators -- Hook provider implementations -- Integration with authorization system -- Request/response handling -- Error management ## License @@ -572,3 +251,4 @@ For a complete working example, refer to the test suite in `src/__tests__/` whic - GitHub Issues: [sourcefuse/loopback4-mcp/issues](https://github.com/sourcefuse/loopback4-mcp/issues) - Documentation: [loopback.io](https://loopback.io/) +