@malek0512/loopback4-mcp 1.0.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.
Files changed (112) hide show
  1. package/README.md +124 -0
  2. package/dist/component.d.ts +8 -0
  3. package/dist/component.d.ts.map +1 -0
  4. package/dist/component.js +67 -0
  5. package/dist/component.js.map +1 -0
  6. package/dist/controllers/index.d.ts +2 -0
  7. package/dist/controllers/index.d.ts.map +1 -0
  8. package/dist/controllers/index.js +18 -0
  9. package/dist/controllers/index.js.map +1 -0
  10. package/dist/controllers/mcp.controller.d.ts +16 -0
  11. package/dist/controllers/mcp.controller.d.ts.map +1 -0
  12. package/dist/controllers/mcp.controller.js +66 -0
  13. package/dist/controllers/mcp.controller.js.map +1 -0
  14. package/dist/decorators/index.d.ts +2 -0
  15. package/dist/decorators/index.d.ts.map +1 -0
  16. package/dist/decorators/index.js +18 -0
  17. package/dist/decorators/index.js.map +1 -0
  18. package/dist/decorators/mcp-tool.decorator.d.ts +21 -0
  19. package/dist/decorators/mcp-tool.decorator.d.ts.map +1 -0
  20. package/dist/decorators/mcp-tool.decorator.js +30 -0
  21. package/dist/decorators/mcp-tool.decorator.js.map +1 -0
  22. package/dist/execution/context-manager.d.ts +14 -0
  23. package/dist/execution/context-manager.d.ts.map +1 -0
  24. package/dist/execution/context-manager.js +41 -0
  25. package/dist/execution/context-manager.js.map +1 -0
  26. package/dist/execution/index.d.ts +4 -0
  27. package/dist/execution/index.d.ts.map +1 -0
  28. package/dist/execution/index.js +20 -0
  29. package/dist/execution/index.js.map +1 -0
  30. package/dist/execution/result-formatter.d.ts +16 -0
  31. package/dist/execution/result-formatter.d.ts.map +1 -0
  32. package/dist/execution/result-formatter.js +102 -0
  33. package/dist/execution/result-formatter.js.map +1 -0
  34. package/dist/execution/tool-executor.d.ts +12 -0
  35. package/dist/execution/tool-executor.d.ts.map +1 -0
  36. package/dist/execution/tool-executor.js +72 -0
  37. package/dist/execution/tool-executor.js.map +1 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +25 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/observers/index.d.ts +2 -0
  43. package/dist/observers/index.d.ts.map +1 -0
  44. package/dist/observers/index.js +18 -0
  45. package/dist/observers/index.js.map +1 -0
  46. package/dist/observers/mcp-boot.observer.d.ts +9 -0
  47. package/dist/observers/mcp-boot.observer.d.ts.map +1 -0
  48. package/dist/observers/mcp-boot.observer.js +36 -0
  49. package/dist/observers/mcp-boot.observer.js.map +1 -0
  50. package/dist/protocol/index.d.ts +4 -0
  51. package/dist/protocol/index.d.ts.map +1 -0
  52. package/dist/protocol/index.js +20 -0
  53. package/dist/protocol/index.js.map +1 -0
  54. package/dist/protocol/json-rpc.handler.d.ts +10 -0
  55. package/dist/protocol/json-rpc.handler.d.ts.map +1 -0
  56. package/dist/protocol/json-rpc.handler.js +63 -0
  57. package/dist/protocol/json-rpc.handler.js.map +1 -0
  58. package/dist/protocol/message-router.d.ts +18 -0
  59. package/dist/protocol/message-router.d.ts.map +1 -0
  60. package/dist/protocol/message-router.js +106 -0
  61. package/dist/protocol/message-router.js.map +1 -0
  62. package/dist/protocol/types.d.ts +227 -0
  63. package/dist/protocol/types.d.ts.map +1 -0
  64. package/dist/protocol/types.js +56 -0
  65. package/dist/protocol/types.js.map +1 -0
  66. package/dist/registry/index.d.ts +4 -0
  67. package/dist/registry/index.d.ts.map +1 -0
  68. package/dist/registry/index.js +20 -0
  69. package/dist/registry/index.js.map +1 -0
  70. package/dist/registry/metadata-scanner.d.ts +13 -0
  71. package/dist/registry/metadata-scanner.d.ts.map +1 -0
  72. package/dist/registry/metadata-scanner.js +101 -0
  73. package/dist/registry/metadata-scanner.js.map +1 -0
  74. package/dist/registry/schema-generator.d.ts +19 -0
  75. package/dist/registry/schema-generator.d.ts.map +1 -0
  76. package/dist/registry/schema-generator.js +90 -0
  77. package/dist/registry/schema-generator.js.map +1 -0
  78. package/dist/registry/tool-registry.d.ts +23 -0
  79. package/dist/registry/tool-registry.d.ts.map +1 -0
  80. package/dist/registry/tool-registry.js +58 -0
  81. package/dist/registry/tool-registry.js.map +1 -0
  82. package/dist/transport/index.d.ts +2 -0
  83. package/dist/transport/index.d.ts.map +1 -0
  84. package/dist/transport/index.js +18 -0
  85. package/dist/transport/index.js.map +1 -0
  86. package/dist/transport/sse.transport.d.ts +9 -0
  87. package/dist/transport/sse.transport.d.ts.map +1 -0
  88. package/dist/transport/sse.transport.js +67 -0
  89. package/dist/transport/sse.transport.js.map +1 -0
  90. package/package.json +26 -0
  91. package/src/component.ts +58 -0
  92. package/src/controllers/index.ts +1 -0
  93. package/src/controllers/mcp.controller.ts +42 -0
  94. package/src/decorators/index.ts +1 -0
  95. package/src/decorators/mcp-tool.decorator.ts +57 -0
  96. package/src/execution/context-manager.ts +44 -0
  97. package/src/execution/index.ts +3 -0
  98. package/src/execution/result-formatter.ts +104 -0
  99. package/src/execution/tool-executor.ts +61 -0
  100. package/src/index.ts +5 -0
  101. package/src/observers/index.ts +1 -0
  102. package/src/observers/mcp-boot.observer.ts +19 -0
  103. package/src/protocol/index.ts +3 -0
  104. package/src/protocol/json-rpc.handler.ts +77 -0
  105. package/src/protocol/message-router.ts +125 -0
  106. package/src/protocol/types.ts +309 -0
  107. package/src/registry/index.ts +3 -0
  108. package/src/registry/metadata-scanner.ts +105 -0
  109. package/src/registry/schema-generator.ts +82 -0
  110. package/src/registry/tool-registry.ts +57 -0
  111. package/src/transport/index.ts +1 -0
  112. package/src/transport/sse.transport.ts +56 -0
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ToolRegistry = void 0;
16
+ const core_1 = require("@loopback/core");
17
+ const schema_generator_1 = require("./schema-generator");
18
+ let ToolRegistry = class ToolRegistry {
19
+ constructor(schemaGenerator) {
20
+ this.schemaGenerator = schemaGenerator;
21
+ this.tools = new Map();
22
+ }
23
+ register(descriptor) {
24
+ if (this.tools.has(descriptor.name)) {
25
+ throw new Error(`Tool '${descriptor.name}' is already registered`);
26
+ }
27
+ this.tools.set(descriptor.name, descriptor);
28
+ }
29
+ unregister(name) {
30
+ return this.tools.delete(name);
31
+ }
32
+ get(name) {
33
+ return this.tools.get(name);
34
+ }
35
+ list() {
36
+ return Array.from(this.tools.values()).map(descriptor => ({
37
+ name: descriptor.name,
38
+ description: descriptor.description,
39
+ inputSchema: descriptor.inputSchema,
40
+ }));
41
+ }
42
+ has(name) {
43
+ return this.tools.has(name);
44
+ }
45
+ clear() {
46
+ this.tools.clear();
47
+ }
48
+ get size() {
49
+ return this.tools.size;
50
+ }
51
+ };
52
+ exports.ToolRegistry = ToolRegistry;
53
+ exports.ToolRegistry = ToolRegistry = __decorate([
54
+ (0, core_1.bind)({ scope: core_1.BindingScope.SINGLETON }),
55
+ __param(0, (0, core_1.inject)('mcp.schemaGenerator')),
56
+ __metadata("design:paramtypes", [schema_generator_1.SchemaGenerator])
57
+ ], ToolRegistry);
58
+ //# sourceMappingURL=tool-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-registry.js","sourceRoot":"","sources":["../../src/registry/tool-registry.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,yCAA0D;AAE1D,yDAAmD;AAY5C,IAAM,YAAY,GAAlB,MAAM,YAAY;IAGvB,YAEE,eAAwC;QAAhC,oBAAe,GAAf,eAAe,CAAiB;QAJlC,UAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAK/C,CAAC;IAEJ,QAAQ,CAAC,UAA0B;QACjC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,SAAS,UAAU,CAAC,IAAI,yBAAyB,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC9C,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI;QACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YACxD,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,WAAW,EAAE,UAAU,CAAC,WAAW;YACnC,WAAW,EAAE,UAAU,CAAC,WAAkB;SAC3C,CAAC,CAAC,CAAC;IACN,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF,CAAA;AA1CY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,WAAI,EAAC,EAAC,KAAK,EAAE,mBAAY,CAAC,SAAS,EAAC,CAAC;IAKjC,WAAA,IAAA,aAAM,EAAC,qBAAqB,CAAC,CAAA;qCACL,kCAAe;GAL/B,YAAY,CA0CxB"}
@@ -0,0 +1,2 @@
1
+ export * from './sse.transport';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./sse.transport"), exports);
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/transport/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,kDAAgC"}
@@ -0,0 +1,9 @@
1
+ import { Request, Response } from '@loopback/rest';
2
+ import { MessageRouter } from '../protocol/message-router';
3
+ export declare class SseTransport {
4
+ private messageRouter;
5
+ constructor(messageRouter: MessageRouter);
6
+ handleConnection(req: Request, res: Response): Promise<void>;
7
+ private sendEvent;
8
+ }
9
+ //# sourceMappingURL=sse.transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.transport.d.ts","sourceRoot":"","sources":["../../src/transport/sse.transport.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AAGzD,qBACa,YAAY;IAGrB,OAAO,CAAC,aAAa;gBAAb,aAAa,EAAE,aAAa;IAGhC,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAuClE,OAAO,CAAC,SAAS;CAIlB"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SseTransport = void 0;
16
+ const core_1 = require("@loopback/core");
17
+ const message_router_1 = require("../protocol/message-router");
18
+ let SseTransport = class SseTransport {
19
+ constructor(messageRouter) {
20
+ this.messageRouter = messageRouter;
21
+ }
22
+ async handleConnection(req, res) {
23
+ // Set SSE headers
24
+ res.setHeader('Content-Type', 'text/event-stream');
25
+ res.setHeader('Cache-Control', 'no-cache');
26
+ res.setHeader('Connection', 'keep-alive');
27
+ res.setHeader('Access-Control-Allow-Origin', '*');
28
+ // Send initial connection event
29
+ this.sendEvent(res, 'connected', { message: 'MCP Server connected' });
30
+ // Handle incoming messages
31
+ req.on('data', async (chunk) => {
32
+ try {
33
+ const message = JSON.parse(chunk.toString());
34
+ const response = await this.messageRouter.route(message);
35
+ this.sendEvent(res, 'message', response);
36
+ }
37
+ catch (error) {
38
+ this.sendEvent(res, 'error', {
39
+ message: 'Failed to process message',
40
+ error: error instanceof Error ? error.message : String(error),
41
+ });
42
+ }
43
+ });
44
+ // Handle connection close
45
+ req.on('close', () => {
46
+ console.log('SSE connection closed');
47
+ });
48
+ // Keep connection alive
49
+ const keepAliveInterval = setInterval(() => {
50
+ this.sendEvent(res, 'ping', { timestamp: Date.now() });
51
+ }, 30000);
52
+ req.on('close', () => {
53
+ clearInterval(keepAliveInterval);
54
+ });
55
+ }
56
+ sendEvent(res, event, data) {
57
+ res.write(`event: ${event}\n`);
58
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
59
+ }
60
+ };
61
+ exports.SseTransport = SseTransport;
62
+ exports.SseTransport = SseTransport = __decorate([
63
+ (0, core_1.bind)({ scope: core_1.BindingScope.SINGLETON }),
64
+ __param(0, (0, core_1.inject)('mcp.messageRouter')),
65
+ __metadata("design:paramtypes", [message_router_1.MessageRouter])
66
+ ], SseTransport);
67
+ //# sourceMappingURL=sse.transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.transport.js","sourceRoot":"","sources":["../../src/transport/sse.transport.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,yCAA0D;AAE1D,+DAAyD;AAIlD,IAAM,YAAY,GAAlB,MAAM,YAAY;IACvB,YAEU,aAA4B;QAA5B,kBAAa,GAAb,aAAa,CAAe;IACnC,CAAC;IAEJ,KAAK,CAAC,gBAAgB,CAAC,GAAY,EAAE,GAAa;QAChD,kBAAkB;QAClB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;QACnD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAC3C,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC1C,GAAG,CAAC,SAAS,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;QAElD,gCAAgC;QAChC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,EAAC,OAAO,EAAE,sBAAsB,EAAC,CAAC,CAAC;QAEpE,2BAA2B;QAC3B,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAa,EAAE,EAAE;YACrC,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAmB,CAAC;gBAC/D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACzD,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE;oBAC3B,OAAO,EAAE,2BAA2B;oBACpC,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;iBAC9D,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,0BAA0B;QAC1B,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACnB,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,wBAAwB;QACxB,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAG,EAAE;YACzC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,EAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAC,CAAC,CAAC;QACvD,CAAC,EAAE,KAAK,CAAC,CAAC;QAEV,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACnB,aAAa,CAAC,iBAAiB,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,SAAS,CAAC,GAAa,EAAE,KAAa,EAAE,IAAa;QAC3D,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC;QAC/B,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAjDY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,WAAI,EAAC,EAAC,KAAK,EAAE,mBAAY,CAAC,SAAS,EAAC,CAAC;IAGjC,WAAA,IAAA,aAAM,EAAC,mBAAmB,CAAC,CAAA;qCACL,8BAAa;GAH3B,YAAY,CAiDxB"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@malek0512/loopback4-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP (Model Context Protocol) component for LoopBack 4 — exposes your controllers as AI-callable tools via JSON-RPC 2.0 + SSE",
5
+ "keywords": ["loopback", "loopback4", "mcp", "model-context-protocol", "ai", "llm", "tools"],
6
+ "license": "MIT",
7
+ "author": "malek0512",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": ["dist", "src"],
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "peerDependencies": {
16
+ "@loopback/core": ">=4.0.0",
17
+ "@loopback/rest": ">=12.0.0",
18
+ "@loopback/openapi-v3": ">=9.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.0.0",
22
+ "@loopback/core": "^6.1.7",
23
+ "@loopback/rest": "^14.0.10",
24
+ "@loopback/openapi-v3": "^10.0.10"
25
+ }
26
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ Component,
3
+ ContextTags,
4
+ injectable,
5
+ CoreBindings,
6
+ inject,
7
+ Application,
8
+ BindingScope,
9
+ } from '@loopback/core';
10
+ import {McpController} from './controllers/mcp.controller';
11
+ import {ToolRegistry} from './registry/tool-registry';
12
+ import {MetadataScanner} from './registry/metadata-scanner';
13
+ import {SchemaGenerator} from './registry/schema-generator';
14
+ import {JsonRpcHandler} from './protocol/json-rpc.handler';
15
+ import {MessageRouter} from './protocol/message-router';
16
+ import {ToolExecutor} from './execution/tool-executor';
17
+ import {ContextManager} from './execution/context-manager';
18
+ import {ResultFormatter} from './execution/result-formatter';
19
+ import {SseTransport} from './transport/sse.transport';
20
+
21
+ @injectable({
22
+ tags: {[ContextTags.KEY]: 'components.McpComponent'},
23
+ })
24
+ export class McpComponent implements Component {
25
+ controllers = [McpController];
26
+
27
+ constructor(
28
+ @inject(CoreBindings.APPLICATION_INSTANCE)
29
+ private app: Application,
30
+ ) {
31
+ // Bind all MCP services as singletons so state (tool registry, initialized flag) persists across requests
32
+ this.app.bind('mcp.toolRegistry').toClass(ToolRegistry).inScope(BindingScope.SINGLETON);
33
+ this.app.bind('mcp.metadataScanner').toClass(MetadataScanner).inScope(BindingScope.SINGLETON);
34
+ this.app.bind('mcp.schemaGenerator').toClass(SchemaGenerator).inScope(BindingScope.SINGLETON);
35
+ this.app.bind('mcp.jsonRpcHandler').toClass(JsonRpcHandler).inScope(BindingScope.SINGLETON);
36
+ this.app.bind('mcp.messageRouter').toClass(MessageRouter).inScope(BindingScope.SINGLETON);
37
+ this.app.bind('mcp.toolExecutor').toClass(ToolExecutor).inScope(BindingScope.SINGLETON);
38
+ this.app.bind('mcp.contextManager').toClass(ContextManager).inScope(BindingScope.SINGLETON);
39
+ this.app.bind('mcp.resultFormatter').toClass(ResultFormatter).inScope(BindingScope.SINGLETON);
40
+ this.app.bind('mcp.sseTransport').toClass(SseTransport).inScope(BindingScope.SINGLETON);
41
+
42
+ // Scan for MCP tools after application starts
43
+ this.app.on('started', async () => {
44
+ // Delay scanning to ensure all controllers are fully initialized
45
+ setTimeout(async () => {
46
+ try {
47
+ console.log('[MCP] Scanning for MCP tools...');
48
+ const scanner = await this.app.get<MetadataScanner>('mcp.metadataScanner');
49
+ await scanner.scan();
50
+ const registry = await this.app.get<ToolRegistry>('mcp.toolRegistry');
51
+ console.log(`[MCP] Tool scanning complete. Found ${registry.size} tools.`);
52
+ } catch (error) {
53
+ console.error('[MCP] Error scanning tools:', error);
54
+ }
55
+ }, 100);
56
+ });
57
+ }
58
+ }
@@ -0,0 +1 @@
1
+ export * from './mcp.controller';
@@ -0,0 +1,42 @@
1
+ import {inject} from '@loopback/core';
2
+ import {
3
+ post,
4
+ get,
5
+ Request,
6
+ Response,
7
+ RestBindings,
8
+ requestBody,
9
+ } from '@loopback/rest';
10
+ import {MessageRouter} from '../protocol/message-router';
11
+ import {SseTransport} from '../transport/sse.transport';
12
+ import {JsonRpcRequest} from '../protocol/types';
13
+
14
+ export class McpController {
15
+ constructor(
16
+ @inject('mcp.messageRouter')
17
+ private messageRouter: MessageRouter,
18
+ @inject('mcp.sseTransport')
19
+ private sseTransport: SseTransport,
20
+ ) {}
21
+
22
+ @post('/mcp')
23
+ async handleMessage(@requestBody() request: JsonRpcRequest): Promise<unknown> {
24
+ return this.messageRouter.route(request);
25
+ }
26
+
27
+ @get('/mcp/sse')
28
+ async handleSse(
29
+ @inject(RestBindings.Http.REQUEST) req: Request,
30
+ @inject(RestBindings.Http.RESPONSE) res: Response,
31
+ ): Promise<void> {
32
+ return this.sseTransport.handleConnection(req, res);
33
+ }
34
+
35
+ @get('/mcp/health')
36
+ async health(): Promise<{status: string; timestamp: number}> {
37
+ return {
38
+ status: 'ok',
39
+ timestamp: Date.now(),
40
+ };
41
+ }
42
+ }
@@ -0,0 +1 @@
1
+ export * from './mcp-tool.decorator';
@@ -0,0 +1,57 @@
1
+ import {MetadataInspector, MethodDecoratorFactory} from '@loopback/core';
2
+ import {JsonSchema} from '../protocol/types';
3
+
4
+ export const MCP_TOOL_METADATA_KEY = 'mcp:tool';
5
+
6
+ export interface McpToolMetadata {
7
+ name: string;
8
+ description?: string;
9
+ inputSchema?: JsonSchema;
10
+ preHook?: {
11
+ binding?: string;
12
+ function?: Function;
13
+ };
14
+ postHook?: {
15
+ binding?: string;
16
+ function?: Function;
17
+ };
18
+ }
19
+
20
+ export function mcpTool(metadata: McpToolMetadata): MethodDecorator {
21
+ return MethodDecoratorFactory.createDecorator<McpToolMetadata>(
22
+ MCP_TOOL_METADATA_KEY,
23
+ metadata,
24
+ {decoratorName: '@mcpTool'},
25
+ );
26
+ }
27
+
28
+ export namespace McpToolMetadata {
29
+ export function getMetadata(
30
+ target: Object,
31
+ methodName: string,
32
+ ): McpToolMetadata | undefined {
33
+ return MetadataInspector.getMethodMetadata<McpToolMetadata>(
34
+ MCP_TOOL_METADATA_KEY,
35
+ target,
36
+ methodName,
37
+ );
38
+ }
39
+
40
+ export function getAllMetadata(target: Object): Map<string, McpToolMetadata> {
41
+ const metadata = new Map<string, McpToolMetadata>();
42
+ const proto = Object.getPrototypeOf(target);
43
+
44
+ const methodNames = Object.getOwnPropertyNames(proto).filter(
45
+ name => typeof proto[name] === 'function' && name !== 'constructor',
46
+ );
47
+
48
+ for (const methodName of methodNames) {
49
+ const toolMetadata = getMetadata(target, methodName);
50
+ if (toolMetadata) {
51
+ metadata.set(methodName, toolMetadata);
52
+ }
53
+ }
54
+
55
+ return metadata;
56
+ }
57
+ }
@@ -0,0 +1,44 @@
1
+ import {bind, BindingScope} from '@loopback/core';
2
+
3
+ export interface ExecutionContext {
4
+ toolName: string;
5
+ arguments: Record<string, unknown>;
6
+ timestamp: number;
7
+ requestId?: string;
8
+ }
9
+
10
+ @bind({scope: BindingScope.SINGLETON})
11
+ export class ContextManager {
12
+ private contexts = new Map<string, ExecutionContext>();
13
+
14
+ createContext(
15
+ toolName: string,
16
+ args: Record<string, unknown>,
17
+ requestId?: string,
18
+ ): ExecutionContext {
19
+ const context: ExecutionContext = {
20
+ toolName,
21
+ arguments: args,
22
+ timestamp: Date.now(),
23
+ requestId,
24
+ };
25
+
26
+ if (requestId) {
27
+ this.contexts.set(requestId, context);
28
+ }
29
+
30
+ return context;
31
+ }
32
+
33
+ getContext(requestId: string): ExecutionContext | undefined {
34
+ return this.contexts.get(requestId);
35
+ }
36
+
37
+ clearContext(requestId: string): boolean {
38
+ return this.contexts.delete(requestId);
39
+ }
40
+
41
+ clearAll(): void {
42
+ this.contexts.clear();
43
+ }
44
+ }
@@ -0,0 +1,3 @@
1
+ export * from './tool-executor';
2
+ export * from './context-manager';
3
+ export * from './result-formatter';
@@ -0,0 +1,104 @@
1
+ import {bind, BindingScope} from '@loopback/core';
2
+ import {Content, TextContent} from '../protocol/types';
3
+
4
+ @bind({scope: BindingScope.SINGLETON})
5
+ export class ResultFormatter {
6
+ /**
7
+ * Format the result of a tool execution into MCP Content format
8
+ */
9
+ format(result: unknown): Content[] {
10
+ // If result already has content array, use it
11
+ if (this.isContentArray(result)) {
12
+ return result as Content[];
13
+ }
14
+
15
+ // If result has a content property, use it
16
+ if (this.hasContentProperty(result)) {
17
+ return (result as any).content;
18
+ }
19
+
20
+ // Convert result to text content
21
+ return this.toTextContent(result);
22
+ }
23
+
24
+ /**
25
+ * Format an error into MCP Content format
26
+ */
27
+ formatError(error: unknown): Content[] {
28
+ let message = 'Unknown error occurred';
29
+ let details: string | undefined;
30
+
31
+ if (error instanceof Error) {
32
+ message = error.message;
33
+ details = error.stack;
34
+ } else if (typeof error === 'string') {
35
+ message = error;
36
+ } else if (error && typeof error === 'object') {
37
+ message = (error as any).message || JSON.stringify(error);
38
+ }
39
+
40
+ const content: TextContent[] = [
41
+ {
42
+ type: 'text',
43
+ text: `Error: ${message}`,
44
+ },
45
+ ];
46
+
47
+ if (details) {
48
+ content.push({
49
+ type: 'text',
50
+ text: `Details:\n${details}`,
51
+ });
52
+ }
53
+
54
+ return content;
55
+ }
56
+
57
+ private isContentArray(result: unknown): boolean {
58
+ return (
59
+ Array.isArray(result) &&
60
+ result.length > 0 &&
61
+ result.every(item => this.isContent(item))
62
+ );
63
+ }
64
+
65
+ private isContent(item: unknown): boolean {
66
+ if (!item || typeof item !== 'object') return false;
67
+ const content = item as any;
68
+ return (
69
+ content.type === 'text' ||
70
+ content.type === 'image' ||
71
+ content.type === 'resource'
72
+ );
73
+ }
74
+
75
+ private hasContentProperty(result: unknown): boolean {
76
+ return (
77
+ result !== null &&
78
+ typeof result === 'object' &&
79
+ 'content' in result &&
80
+ Array.isArray((result as any).content)
81
+ );
82
+ }
83
+
84
+ private toTextContent(result: unknown): Content[] {
85
+ let text: string;
86
+
87
+ if (result === null || result === undefined) {
88
+ text = 'Operation completed successfully';
89
+ } else if (typeof result === 'string') {
90
+ text = result;
91
+ } else if (typeof result === 'object') {
92
+ text = JSON.stringify(result, null, 2);
93
+ } else {
94
+ text = String(result);
95
+ }
96
+
97
+ return [
98
+ {
99
+ type: 'text',
100
+ text,
101
+ },
102
+ ];
103
+ }
104
+ }
@@ -0,0 +1,61 @@
1
+ import {bind, BindingScope, inject} from '@loopback/core';
2
+ import {ToolRegistry} from '../registry/tool-registry';
3
+ import {ToolCallResult, McpErrorCode} from '../protocol/types';
4
+ import {ContextManager} from './context-manager';
5
+ import {ResultFormatter} from './result-formatter';
6
+
7
+ @bind({scope: BindingScope.SINGLETON})
8
+ export class ToolExecutor {
9
+ constructor(
10
+ @inject('mcp.toolRegistry')
11
+ private toolRegistry: ToolRegistry,
12
+ @inject('mcp.contextManager')
13
+ private contextManager: ContextManager,
14
+ @inject('mcp.resultFormatter')
15
+ private resultFormatter: ResultFormatter,
16
+ ) {}
17
+
18
+ async execute(
19
+ toolName: string,
20
+ args: Record<string, unknown>,
21
+ ): Promise<ToolCallResult> {
22
+ // Get tool descriptor
23
+ const descriptor = this.toolRegistry.get(toolName);
24
+ if (!descriptor) {
25
+ throw {
26
+ code: McpErrorCode.TOOL_NOT_FOUND,
27
+ message: `Tool '${toolName}' not found`,
28
+ };
29
+ }
30
+
31
+ try {
32
+ // Create execution context
33
+ const context = this.contextManager.createContext(toolName, args);
34
+
35
+ // Execute pre-hooks (if any)
36
+ // await this.executePreHooks(context);
37
+
38
+ // Execute the tool
39
+ const result = await descriptor.handler(args);
40
+
41
+ // Execute post-hooks (if any)
42
+ // await this.executePostHooks(context, result);
43
+
44
+ // Format result
45
+ const formattedResult = this.resultFormatter.format(result);
46
+
47
+ return {
48
+ content: formattedResult,
49
+ isError: false,
50
+ };
51
+ } catch (error) {
52
+ // Format error
53
+ const errorContent = this.resultFormatter.formatError(error);
54
+
55
+ return {
56
+ content: errorContent,
57
+ isError: true,
58
+ };
59
+ }
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export {McpComponent} from './component';
2
+ export {mcpTool} from './decorators/mcp-tool.decorator';
3
+ export type {McpToolMetadata} from './decorators/mcp-tool.decorator';
4
+ export {MCP_TOOL_METADATA_KEY} from './decorators/mcp-tool.decorator';
5
+ export * from './protocol/types';
@@ -0,0 +1 @@
1
+ export * from './mcp-boot.observer';
@@ -0,0 +1,19 @@
1
+ import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
2
+ import {MetadataScanner} from '../registry/metadata-scanner';
3
+
4
+ @lifeCycleObserver('mcp')
5
+ export class McpBootObserver implements LifeCycleObserver {
6
+ constructor(
7
+ @inject('mcp.metadataScanner')
8
+ private metadataScanner: MetadataScanner,
9
+ ) {}
10
+
11
+ async start(): Promise<void> {
12
+ // Note: Actual scanning happens in component after 'started' event
13
+ // This observer is kept for future lifecycle hooks if needed
14
+ }
15
+
16
+ async stop(): Promise<void> {
17
+ console.log('[MCP] Shutting down MCP component');
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './json-rpc.handler';
3
+ export * from './message-router';