loopback4-mcp 0.0.1

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 (87) hide show
  1. package/README.md +87 -0
  2. package/dist/component.d.ts +11 -0
  3. package/dist/component.js +22 -0
  4. package/dist/component.js.map +1 -0
  5. package/dist/constants/index.d.ts +1 -0
  6. package/dist/constants/index.js +5 -0
  7. package/dist/constants/index.js.map +1 -0
  8. package/dist/constants/mcp-tool.constant.d.ts +1 -0
  9. package/dist/constants/mcp-tool.constant.js +6 -0
  10. package/dist/constants/mcp-tool.constant.js.map +1 -0
  11. package/dist/controllers/index.d.ts +1 -0
  12. package/dist/controllers/index.js +5 -0
  13. package/dist/controllers/index.js.map +1 -0
  14. package/dist/controllers/mcp.controller.d.ts +10 -0
  15. package/dist/controllers/mcp.controller.js +121 -0
  16. package/dist/controllers/mcp.controller.js.map +1 -0
  17. package/dist/decorators/index.d.ts +1 -0
  18. package/dist/decorators/index.js +5 -0
  19. package/dist/decorators/index.js.map +1 -0
  20. package/dist/decorators/mcp-tool.decorator.d.ts +2 -0
  21. package/dist/decorators/mcp-tool.decorator.js +45 -0
  22. package/dist/decorators/mcp-tool.decorator.js.map +1 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +9 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/interfaces/index.d.ts +1 -0
  27. package/dist/interfaces/index.js +5 -0
  28. package/dist/interfaces/index.js.map +1 -0
  29. package/dist/interfaces/mcp-hook-provider.interface.d.ts +12 -0
  30. package/dist/interfaces/mcp-hook-provider.interface.js +3 -0
  31. package/dist/interfaces/mcp-hook-provider.interface.js.map +1 -0
  32. package/dist/keys.d.ts +1 -0
  33. package/dist/keys.js +3 -0
  34. package/dist/keys.js.map +1 -0
  35. package/dist/observers/index.d.ts +1 -0
  36. package/dist/observers/index.js +5 -0
  37. package/dist/observers/index.js.map +1 -0
  38. package/dist/observers/mcp-tool-registry-boot.observer.d.ts +19 -0
  39. package/dist/observers/mcp-tool-registry-boot.observer.js +42 -0
  40. package/dist/observers/mcp-tool-registry-boot.observer.js.map +1 -0
  41. package/dist/services/index.d.ts +3 -0
  42. package/dist/services/index.js +7 -0
  43. package/dist/services/index.js.map +1 -0
  44. package/dist/services/mcp-schema-generator-service.service.d.ts +20 -0
  45. package/dist/services/mcp-schema-generator-service.service.js +73 -0
  46. package/dist/services/mcp-schema-generator-service.service.js.map +1 -0
  47. package/dist/services/mcp-server-factory.service.d.ts +13 -0
  48. package/dist/services/mcp-server-factory.service.js +51 -0
  49. package/dist/services/mcp-server-factory.service.js.map +1 -0
  50. package/dist/services/mcp-tool-registry.service.d.ts +49 -0
  51. package/dist/services/mcp-tool-registry.service.js +245 -0
  52. package/dist/services/mcp-tool-registry.service.js.map +1 -0
  53. package/dist/types.d.ts +46 -0
  54. package/dist/types.js +17 -0
  55. package/dist/types.js.map +1 -0
  56. package/dist/utils/index.d.ts +1 -0
  57. package/dist/utils/index.js +5 -0
  58. package/dist/utils/index.js.map +1 -0
  59. package/dist/utils/mcp-parameter-extractor.d.ts +9 -0
  60. package/dist/utils/mcp-parameter-extractor.js +51 -0
  61. package/dist/utils/mcp-parameter-extractor.js.map +1 -0
  62. package/package.json +139 -0
  63. package/src/component.ts +18 -0
  64. package/src/constants/index.ts +1 -0
  65. package/src/constants/mcp-tool.constant.ts +2 -0
  66. package/src/controllers/README.md +6 -0
  67. package/src/controllers/index.ts +1 -0
  68. package/src/controllers/mcp.controller.ts +112 -0
  69. package/src/decorators/README.md +34 -0
  70. package/src/decorators/index.ts +1 -0
  71. package/src/decorators/mcp-tool.decorator.ts +68 -0
  72. package/src/index.ts +5 -0
  73. package/src/interfaces/index.ts +1 -0
  74. package/src/interfaces/mcp-hook-provider.interface.ts +11 -0
  75. package/src/keys.ts +1 -0
  76. package/src/mixins/README.md +216 -0
  77. package/src/observers/index.ts +1 -0
  78. package/src/observers/mcp-tool-registry-boot.observer.ts +40 -0
  79. package/src/providers/README.md +129 -0
  80. package/src/repositories/README.md +5 -0
  81. package/src/services/index.ts +3 -0
  82. package/src/services/mcp-schema-generator-service.service.ts +80 -0
  83. package/src/services/mcp-server-factory.service.ts +71 -0
  84. package/src/services/mcp-tool-registry.service.ts +368 -0
  85. package/src/types.ts +59 -0
  86. package/src/utils/index.ts +1 -0
  87. package/src/utils/mcp-parameter-extractor.ts +67 -0
@@ -0,0 +1,129 @@
1
+ # Providers
2
+
3
+ This directory contains providers contributing additional bindings, for example
4
+ custom sequence actions.
5
+
6
+ ## Overview
7
+
8
+ A [provider](http://loopback.io/doc/en/lb4/Creating-components.html#providers)
9
+ is a class that provides a `value()` function. This function is called `Context`
10
+ when another entity requests a value to be injected.
11
+
12
+ Here we create a provider for a logging function that can be used as a new
13
+ action in a custom [sequence](http://loopback.io/doc/en/lb4/Sequence.html).
14
+
15
+ The logger will log the URL, the parsed request parameters, and the result. The
16
+ logger is also capable of timing the sequence if you start a timer at the start
17
+ of the sequence using `this.logger.startTimer()`.
18
+
19
+ ## Basic Usage
20
+
21
+ ### TimerProvider
22
+
23
+ TimerProvider is automatically bound to your Application's
24
+ [Context](http://loopback.io/doc/en/lb4/Context.html) using the LogComponent
25
+ which exports this provider with a binding key of `extension-starter.timer`. You
26
+ can learn more about components in the
27
+ [related resources section](#related-resources).
28
+
29
+ This provider makes available to your application a timer function which given a
30
+ start time _(given as an array [seconds, nanoseconds])_ can give you a total
31
+ time elapsed since the start in milliseconds. The timer can also start timing if
32
+ no start time is given. This is used by LogComponent to allow a user to time a
33
+ Sequence.
34
+
35
+ _NOTE:_ _You can get the start time in the required format by using
36
+ `this.logger.startTimer()`._
37
+
38
+ You can provide your own implementation of the elapsed time function by binding
39
+ it to the binding key (accessible via `ExtensionStarterBindings`) as follows:
40
+
41
+ ```ts
42
+ app.bind(ExtensionStarterBindings.TIMER).to(timerFn);
43
+ ```
44
+
45
+ ### LogProvider
46
+
47
+ LogProvider can automatically be bound to your Application's Context using the
48
+ LogComponent which exports the provider with a binding key of
49
+ `extension-starter.actions.log`.
50
+
51
+ The key can be accessed by importing `ExtensionStarterBindings` as follows:
52
+
53
+ **Example: Binding Keys**
54
+
55
+ ```ts
56
+ import {ExtensionStarterBindings} from 'HelloExtensions';
57
+ // Key can be accessed as follows now
58
+ const key = ExtensionStarterBindings.LOG_ACTION;
59
+ ```
60
+
61
+ LogProvider gives us a sequence action and a `startTimer` function. In order to
62
+ use the sequence action, you must define your own sequence as shown below.
63
+
64
+ **Example: Sequence**
65
+
66
+ ```ts
67
+ class LogSequence implements SequenceHandler {
68
+ constructor(
69
+ @inject(coreSequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
70
+ @inject(coreSequenceActions.PARSE_PARAMS)
71
+ protected parseParams: ParseParams,
72
+ @inject(coreSequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
73
+ @inject(coreSequenceActions.SEND) protected send: Send,
74
+ @inject(coreSequenceActions.REJECT) protected reject: Reject,
75
+ // We get the logger injected by the LogProvider here
76
+ @inject(ExtensionStarterBindings.LOG_ACTION) protected logger: LogFn,
77
+ ) {}
78
+
79
+ async handle(context: RequestContext) {
80
+ const {request, response} = context;
81
+
82
+ // We define these variable outside so they can be accessed by logger.
83
+ let args: any = [];
84
+ let result: any;
85
+
86
+ // Optionally start timing the sequence using the timer
87
+ // function available via LogFn
88
+ const start = this.logger.startTimer();
89
+
90
+ try {
91
+ const route = this.findRoute(request);
92
+ args = await this.parseParams(request, route);
93
+ result = await this.invoke(route, args);
94
+ this.send(response, result);
95
+ } catch (error) {
96
+ result = error; // so we can log the error message in the logger
97
+ this.reject(context, error);
98
+ }
99
+
100
+ // We call the logger function given to us by LogProvider
101
+ this.logger(request, args, result, start);
102
+ }
103
+ }
104
+ ```
105
+
106
+ Once a sequence has been written, we can just use that in our Application as
107
+ follows:
108
+
109
+ **Example: Application**
110
+
111
+ ```ts
112
+ const app = new Application({
113
+ sequence: LogSequence,
114
+ });
115
+ app.component(LogComponent);
116
+
117
+ // Now all requests handled by our sequence will be logged.
118
+ ```
119
+
120
+ ## Related Resources
121
+
122
+ You can check out the following resource to learn more about providers,
123
+ components, sequences, and binding keys.
124
+
125
+ - [Providers](http://loopback.io/doc/en/lb4/Creating-components.html#providers)
126
+ - [Creating Components](http://loopback.io/doc/en/lb4/Creating-components.html)
127
+ - [Using Components](http://loopback.io/doc/en/lb4/Components.html)
128
+ - [Sequence](http://loopback.io/doc/en/lb4/Sequence.html)
129
+ - [Binding Keys](http://loopback.io/doc/en/lb4/Decorators.html)
@@ -0,0 +1,5 @@
1
+ # Repositories
2
+
3
+ This directory contains code for repositories provided by this extension.
4
+
5
+ For more information, see <http://loopback.io/doc/en/lb4/Repositories.html>.
@@ -0,0 +1,3 @@
1
+ export * from './mcp-server-factory.service';
2
+ export * from './mcp-tool-registry.service';
3
+ export * from './mcp-schema-generator-service.service';
@@ -0,0 +1,80 @@
1
+ import {BindingScope, inject, injectable} from '@loopback/core';
2
+ import {ILogger, LOGGER} from '@sourceloop/core';
3
+ import {z, ZodRawShape} from 'zod';
4
+ import {extractParameterInfo} from '../utils';
5
+
6
+ @injectable({scope: BindingScope.SINGLETON})
7
+ export class McpSchemaGeneratorService {
8
+ constructor(
9
+ @inject(LOGGER.LOGGER_INJECT)
10
+ private readonly logger: ILogger,
11
+ ) {}
12
+
13
+ /**
14
+ * Create Zod schema for a single parameter using LoopBack type string
15
+ */
16
+ createZodSchemaForParameterType(paramType: string) {
17
+ switch (paramType) {
18
+ case 'string':
19
+ return z.string();
20
+ case 'number':
21
+ return z.number();
22
+ case 'boolean':
23
+ return z.boolean();
24
+ case 'object':
25
+ return z.object({}).passthrough();
26
+ case 'array':
27
+ return z.array(z.any());
28
+ default:
29
+ return z.any();
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Generate tool schema using LoopBack metadata
35
+ */
36
+ generateToolSchemaFromLoopBack(
37
+ options: {name: string; description: string; schema?: ZodRawShape},
38
+ target: Object,
39
+ methodName: string,
40
+ ) {
41
+ let schema = options.schema;
42
+
43
+ if (!schema || Object.keys(schema).length === 0) {
44
+ try {
45
+ const {parameterNames, parameterOptional, parameterTypes} =
46
+ extractParameterInfo(target, methodName);
47
+
48
+ if (parameterNames.length > 0) {
49
+ let combinedSchema = z.object({});
50
+
51
+ parameterTypes.forEach((paramType, index) => {
52
+ const zodSchema = this.createZodSchemaForParameterType(paramType);
53
+ const paramName = parameterNames[index];
54
+ const isOptional = parameterOptional[index];
55
+
56
+ combinedSchema = combinedSchema.merge(
57
+ z.object({
58
+ [paramName]: isOptional ? zodSchema.optional() : zodSchema,
59
+ }),
60
+ );
61
+ });
62
+
63
+ schema = combinedSchema.shape;
64
+ } else {
65
+ schema = {};
66
+ }
67
+ } catch (error) {
68
+ this.logger.warn(
69
+ `Failed to generate schema for tool ${options.name} from LoopBack metadata:`,
70
+ error,
71
+ );
72
+ schema = {};
73
+ }
74
+ } else {
75
+ schema = schema ?? {};
76
+ }
77
+
78
+ return schema;
79
+ }
80
+ }
@@ -0,0 +1,71 @@
1
+ import {bind, BindingScope, Context, inject, service} from '@loopback/core';
2
+ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import {McpToolRegistry} from './mcp-tool-registry.service';
4
+ import {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol';
5
+ import {
6
+ ServerNotification,
7
+ ServerRequest,
8
+ } from '@modelcontextprotocol/sdk/types';
9
+
10
+ @bind({scope: BindingScope.REQUEST})
11
+ export class McpServerFactory {
12
+ constructor(
13
+ @inject.context()
14
+ private readonly ctx: Context,
15
+ @service(McpToolRegistry)
16
+ private readonly toolRegistry: McpToolRegistry,
17
+ ) {}
18
+
19
+ /**
20
+ * Create a new MCP server instance with tool registration
21
+ * Uses singleton registry with pre-computed tools and controller instances
22
+ */
23
+ createServer(): McpServer {
24
+ // Create fresh server instance
25
+ const server = new McpServer(
26
+ {
27
+ name: 'Project management MCP server',
28
+ version: '1.0.0',
29
+ },
30
+ {
31
+ capabilities: {
32
+ tools: {},
33
+ },
34
+ },
35
+ );
36
+
37
+ // Tool registration from singleton registry
38
+ const toolDefinitions = this.toolRegistry.getToolDefinitions();
39
+ for (const toolDef of toolDefinitions) {
40
+ // Adapt the registry handler to work with the new API signature
41
+ // The new API expects (parameters, extra) instead of (context, args, extras)
42
+ const adaptedHandler = async (
43
+ parameters: Record<string, unknown>,
44
+ extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
45
+ ) => toolDef.handler(this.ctx, parameters, extra);
46
+
47
+ // Use the new registerTool API with type assertion to avoid deep type recursion
48
+ const registerTool = (
49
+ server as unknown as {
50
+ registerTool(
51
+ name: string,
52
+ config: {description: string; inputSchema: unknown},
53
+ handler: Function,
54
+ ): void;
55
+ }
56
+ ).registerTool;
57
+
58
+ registerTool.call(
59
+ server,
60
+ toolDef.name,
61
+ {
62
+ description: toolDef.description,
63
+ inputSchema: toolDef.schema,
64
+ },
65
+ adaptedHandler,
66
+ );
67
+ }
68
+
69
+ return server;
70
+ }
71
+ }
@@ -0,0 +1,368 @@
1
+ import {
2
+ Application,
3
+ bind,
4
+ BindingKey,
5
+ BindingScope,
6
+ Constructor,
7
+ Context,
8
+ CoreBindings,
9
+ inject,
10
+ MetadataInspector,
11
+ service,
12
+ } from '@loopback/core';
13
+ import {HttpErrors} from '@loopback/rest';
14
+ import {RequestHandlerExtra} from '@modelcontextprotocol/sdk/shared/protocol';
15
+ import {
16
+ CallToolResult,
17
+ ServerNotification,
18
+ ServerRequest,
19
+ } from '@modelcontextprotocol/sdk/types';
20
+ import {IAuthUserWithPermissions, ILogger, LOGGER} from '@sourceloop/core';
21
+ import {AuthenticationBindings} from 'loopback4-authentication';
22
+ import {
23
+ AUTHORIZATION_METADATA_ACCESSOR,
24
+ AuthorizationBindings,
25
+ AuthorizationMetadata,
26
+ } from 'loopback4-authorization';
27
+ import {McpHookContext, McpHookFunction} from '../interfaces';
28
+ import {MCP_TOOL_METADATA_KEY} from '../constants';
29
+ import {extractParameterInfo} from '../utils';
30
+ import {McpSchemaGeneratorService} from './mcp-schema-generator-service.service';
31
+ import {McpTool, McpToolMetadata} from '../types';
32
+
33
+ @bind({scope: BindingScope.SINGLETON})
34
+ export class McpToolRegistry {
35
+ private toolDefinitions: McpTool[] = [];
36
+ private isInitialized = false;
37
+
38
+ constructor(
39
+ @inject(CoreBindings.APPLICATION_INSTANCE)
40
+ private readonly app: Application,
41
+ @inject(LOGGER.LOGGER_INJECT)
42
+ private readonly logger: ILogger,
43
+ @service(McpSchemaGeneratorService)
44
+ private readonly schemaGenerator: McpSchemaGeneratorService,
45
+ ) {}
46
+
47
+ /**
48
+ * Get MCP tools from a class using LoopBack MetadataInspector
49
+ */
50
+ private getMcpToolsFromClass(targetClass: Function): McpToolMetadata[] {
51
+ const processedTools: McpToolMetadata[] = [];
52
+ const prototype = targetClass.prototype;
53
+
54
+ const allTools = MetadataInspector.getAllMethodMetadata<McpToolMetadata>(
55
+ MCP_TOOL_METADATA_KEY,
56
+ prototype,
57
+ );
58
+
59
+ if (!allTools || Object.keys(allTools).length === 0) {
60
+ return [];
61
+ }
62
+
63
+ for (const [methodName, tool] of Object.entries(allTools)) {
64
+ try {
65
+ const {parameterNames} = extractParameterInfo(prototype, methodName);
66
+
67
+ // Create a copy with updated parameter info
68
+ const processedTool: McpToolMetadata = {
69
+ ...tool,
70
+ parameterNames,
71
+ };
72
+
73
+ // Generate schema if needed
74
+ if (
75
+ !processedTool.schema ||
76
+ Object.keys(processedTool.schema).length === 0
77
+ ) {
78
+ const schema = this.schemaGenerator.generateToolSchemaFromLoopBack(
79
+ {
80
+ name: processedTool.name,
81
+ description: processedTool.description,
82
+ schema: processedTool.schema,
83
+ },
84
+ prototype,
85
+ methodName,
86
+ );
87
+ processedTool.schema = schema;
88
+ }
89
+
90
+ processedTools.push(processedTool);
91
+ } catch (error) {
92
+ // Tool doesn't have LoopBack parameter decorators
93
+ this.logger.warn(
94
+ `Tool ${methodName} missing LoopBack parameter decorators:`,
95
+ error,
96
+ );
97
+ }
98
+ }
99
+
100
+ return processedTools;
101
+ }
102
+
103
+ /**
104
+ * Initialize tool registry at startup - stores metadata without instantiating controllers
105
+ */
106
+ async initialize(): Promise<void> {
107
+ if (this.isInitialized) return;
108
+
109
+ const controllerBindings = this.app.findByTag('controller');
110
+
111
+ // Process all bindings and collect tool metadata without instantiating controllers
112
+ const toolArrays = controllerBindings.map(binding => {
113
+ const controllerClass = binding.valueConstructor;
114
+ if (!controllerClass) {
115
+ this.logger.info(`No constructor found for binding ${binding.key}`);
116
+ return [];
117
+ }
118
+
119
+ const tools = this.getMcpToolsFromClass(controllerClass);
120
+
121
+ if (tools.length === 0) return [];
122
+
123
+ // Create pre-bound tool definitions
124
+ return tools.map(tool => {
125
+ return {
126
+ ...tool,
127
+ controllerBinding: BindingKey.create<object>(binding.key), // Store binding key instead of instance
128
+ handler: async (
129
+ requestContext: Context,
130
+ args: {[key: string]: unknown},
131
+ extras: RequestHandlerExtra<ServerRequest, ServerNotification>,
132
+ ): Promise<CallToolResult> => {
133
+ const ctx = await this.setupAuthorizationContext(
134
+ requestContext,
135
+ tool,
136
+ controllerClass,
137
+ );
138
+
139
+ const hookContext: McpHookContext = {
140
+ toolName: tool.name,
141
+ args: args,
142
+ metadata: tool.postHook?.config,
143
+ };
144
+ await this.executePreHook(ctx, tool, hookContext);
145
+
146
+ let result;
147
+ try {
148
+ result = await this.executeToolMethod(
149
+ ctx,
150
+ binding.key,
151
+ tool,
152
+ hookContext,
153
+ extras,
154
+ );
155
+ hookContext.result = result;
156
+
157
+ await this.executePostHook(ctx, tool, hookContext);
158
+ } catch (err) {
159
+ const error = err instanceof Error ? err : new Error(String(err));
160
+ hookContext.error = error;
161
+ this.logger.error(
162
+ `MCP Tool '${tool.name}' execution failed:`,
163
+ error.message,
164
+ );
165
+ return this.errorToCallToolResult(error);
166
+ } finally {
167
+ ctx.close();
168
+ }
169
+ return (hookContext.result as CallToolResult) ?? result;
170
+ },
171
+ };
172
+ });
173
+ });
174
+
175
+ this.toolDefinitions = toolArrays.flat();
176
+ this.isInitialized = true;
177
+ }
178
+
179
+ /**
180
+ * Get all precomputed tool definitions (ultra-fast)
181
+ */
182
+ getToolDefinitions(): McpTool[] {
183
+ return this.toolDefinitions;
184
+ }
185
+
186
+ /**
187
+ * Resolve hook function from provider binding
188
+ */
189
+ private async resolveHook(
190
+ ctx: Context,
191
+ hookBinding?: BindingKey<McpHookFunction> | string,
192
+ ): Promise<McpHookFunction | undefined> {
193
+ if (!hookBinding) {
194
+ return undefined;
195
+ }
196
+
197
+ try {
198
+ const bindingKey =
199
+ typeof hookBinding === 'string'
200
+ ? BindingKey.create<McpHookFunction>(hookBinding)
201
+ : hookBinding;
202
+ return await ctx.get(bindingKey);
203
+ } catch (error) {
204
+ // Hook binding not found - this is not an error, just log and continue
205
+ this.logger.warn(
206
+ `Hook binding ${hookBinding.toString()} not found:`,
207
+ error,
208
+ );
209
+ return undefined;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Set up authorization context and perform authorization check
215
+ */
216
+ private async setupAuthorizationContext(
217
+ requestContext: Context,
218
+ tool: McpToolMetadata,
219
+ controllerClass: Constructor<object>,
220
+ ): Promise<Context> {
221
+ // Get authorization metadata from the controller method
222
+ const authMetadata =
223
+ MetadataInspector.getMethodMetadata<AuthorizationMetadata>(
224
+ AUTHORIZATION_METADATA_ACCESSOR,
225
+ controllerClass.prototype,
226
+ tool.controllerFunction.name,
227
+ );
228
+
229
+ if (!authMetadata) {
230
+ this.logger.warn(
231
+ `No authorization metadata found for MCP tool '${tool.name}'`,
232
+ );
233
+ throw new HttpErrors.Forbidden(
234
+ `MCP tool '${tool.name}' is missing authorization configuration.`,
235
+ );
236
+ }
237
+
238
+ // Check authorization first - get authorize function from context
239
+ const ctx = new Context(requestContext);
240
+
241
+ // Bind the authorization metadata so the authorize action provider can access it
242
+ ctx.bind(AuthorizationBindings.METADATA).to(authMetadata);
243
+
244
+ // Bind controller information for authorization
245
+ ctx.bind(CoreBindings.CONTROLLER_CLASS).to(controllerClass);
246
+ ctx
247
+ .bind(CoreBindings.CONTROLLER_METHOD_NAME)
248
+ .to(tool.controllerFunction.name);
249
+
250
+ const user = await ctx.get<IAuthUserWithPermissions>(
251
+ AuthenticationBindings.CURRENT_USER,
252
+ );
253
+
254
+ const authorizeAction = await ctx.get(
255
+ AuthorizationBindings.AUTHORIZE_ACTION,
256
+ );
257
+ const isAuthorized = await authorizeAction(user.permissions);
258
+
259
+ if (!isAuthorized) {
260
+ this.logger.warn(
261
+ `User ${user.id} is not authorized to access MCP tool '${tool.name}'`,
262
+ );
263
+ throw new HttpErrors.Forbidden(
264
+ `Access denied for MCP tool '${tool.name}'.`,
265
+ );
266
+ }
267
+
268
+ return ctx;
269
+ }
270
+
271
+ /**
272
+ * Prepare method arguments based on parameter patterns
273
+ */
274
+ private prepareMethodArguments(
275
+ tool: McpToolMetadata,
276
+ args: {[key: string]: unknown},
277
+ ): unknown[] {
278
+ // Extract individual parameter values by name
279
+ if (!tool.parameterNames?.length) {
280
+ // Fallback - pass args object directly if no parameter names
281
+ return [args];
282
+ }
283
+
284
+ // Extract individual values by parameter names
285
+ return tool.parameterNames.map(paramName => args?.[paramName]);
286
+ }
287
+
288
+ /**
289
+ * Execute pre-hook if configured
290
+ */
291
+ private async executePreHook(
292
+ ctx: Context,
293
+ tool: McpToolMetadata,
294
+ hookContext: McpHookContext,
295
+ ): Promise<void> {
296
+ const preHook = await this.resolveHook(ctx, tool.preHook?.binding);
297
+ if (preHook) {
298
+ const preHookResult = await preHook(hookContext);
299
+ if (preHookResult) {
300
+ hookContext.args = preHookResult.args;
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Execute post-hook if configured
307
+ */
308
+ private async executePostHook(
309
+ ctx: Context,
310
+ tool: McpToolMetadata,
311
+ hookContext: McpHookContext,
312
+ ): Promise<void> {
313
+ const postHook = await this.resolveHook(ctx, tool.postHook?.binding);
314
+ if (postHook) {
315
+ const postHookResult = await postHook(hookContext);
316
+ if (postHookResult) {
317
+ hookContext.result = postHookResult.result;
318
+ hookContext.args = postHookResult.args;
319
+ hookContext.error = postHookResult.error;
320
+ }
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Execute the tool method with proper result wrapping
326
+ */
327
+ private async executeToolMethod(
328
+ ctx: Context,
329
+ bindingKey: string,
330
+ tool: McpToolMetadata,
331
+ hookContext: McpHookContext,
332
+ extras: RequestHandlerExtra<ServerRequest, ServerNotification>,
333
+ ): Promise<CallToolResult> {
334
+ const controllerInstance = await ctx.get(bindingKey);
335
+ const methodArgs = this.prepareMethodArguments(tool, hookContext.args);
336
+
337
+ let result = await tool.controllerFunction.call(
338
+ controllerInstance,
339
+ ...methodArgs,
340
+ extras,
341
+ );
342
+
343
+ // Automatically wrap result in MCP response format if not already wrapped
344
+ if (result && typeof result === 'object' && !result.content) {
345
+ result = {
346
+ content: [
347
+ {
348
+ type: 'text',
349
+ text: JSON.stringify(result, null, 2),
350
+ },
351
+ ],
352
+ } as CallToolResult;
353
+ }
354
+
355
+ return result;
356
+ }
357
+
358
+ private errorToCallToolResult(error: Error): CallToolResult {
359
+ return {
360
+ content: [
361
+ {
362
+ type: 'text',
363
+ text: error.message || 'MCP tool execution failed',
364
+ },
365
+ ],
366
+ } as CallToolResult;
367
+ }
368
+ }