@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.
- package/README.md +124 -0
- package/dist/component.d.ts +8 -0
- package/dist/component.d.ts.map +1 -0
- package/dist/component.js +67 -0
- package/dist/component.js.map +1 -0
- package/dist/controllers/index.d.ts +2 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +18 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/mcp.controller.d.ts +16 -0
- package/dist/controllers/mcp.controller.d.ts.map +1 -0
- package/dist/controllers/mcp.controller.js +66 -0
- package/dist/controllers/mcp.controller.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +18 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/mcp-tool.decorator.d.ts +21 -0
- package/dist/decorators/mcp-tool.decorator.d.ts.map +1 -0
- package/dist/decorators/mcp-tool.decorator.js +30 -0
- package/dist/decorators/mcp-tool.decorator.js.map +1 -0
- package/dist/execution/context-manager.d.ts +14 -0
- package/dist/execution/context-manager.d.ts.map +1 -0
- package/dist/execution/context-manager.js +41 -0
- package/dist/execution/context-manager.js.map +1 -0
- package/dist/execution/index.d.ts +4 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +20 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/result-formatter.d.ts +16 -0
- package/dist/execution/result-formatter.d.ts.map +1 -0
- package/dist/execution/result-formatter.js +102 -0
- package/dist/execution/result-formatter.js.map +1 -0
- package/dist/execution/tool-executor.d.ts +12 -0
- package/dist/execution/tool-executor.d.ts.map +1 -0
- package/dist/execution/tool-executor.js +72 -0
- package/dist/execution/tool-executor.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/observers/index.d.ts +2 -0
- package/dist/observers/index.d.ts.map +1 -0
- package/dist/observers/index.js +18 -0
- package/dist/observers/index.js.map +1 -0
- package/dist/observers/mcp-boot.observer.d.ts +9 -0
- package/dist/observers/mcp-boot.observer.d.ts.map +1 -0
- package/dist/observers/mcp-boot.observer.js +36 -0
- package/dist/observers/mcp-boot.observer.js.map +1 -0
- package/dist/protocol/index.d.ts +4 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +20 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/json-rpc.handler.d.ts +10 -0
- package/dist/protocol/json-rpc.handler.d.ts.map +1 -0
- package/dist/protocol/json-rpc.handler.js +63 -0
- package/dist/protocol/json-rpc.handler.js.map +1 -0
- package/dist/protocol/message-router.d.ts +18 -0
- package/dist/protocol/message-router.d.ts.map +1 -0
- package/dist/protocol/message-router.js +106 -0
- package/dist/protocol/message-router.js.map +1 -0
- package/dist/protocol/types.d.ts +227 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +56 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/registry/index.d.ts +4 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +20 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/metadata-scanner.d.ts +13 -0
- package/dist/registry/metadata-scanner.d.ts.map +1 -0
- package/dist/registry/metadata-scanner.js +101 -0
- package/dist/registry/metadata-scanner.js.map +1 -0
- package/dist/registry/schema-generator.d.ts +19 -0
- package/dist/registry/schema-generator.d.ts.map +1 -0
- package/dist/registry/schema-generator.js +90 -0
- package/dist/registry/schema-generator.js.map +1 -0
- package/dist/registry/tool-registry.d.ts +23 -0
- package/dist/registry/tool-registry.d.ts.map +1 -0
- package/dist/registry/tool-registry.js +58 -0
- package/dist/registry/tool-registry.js.map +1 -0
- package/dist/transport/index.d.ts +2 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +18 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/sse.transport.d.ts +9 -0
- package/dist/transport/sse.transport.d.ts.map +1 -0
- package/dist/transport/sse.transport.js +67 -0
- package/dist/transport/sse.transport.js.map +1 -0
- package/package.json +26 -0
- package/src/component.ts +58 -0
- package/src/controllers/index.ts +1 -0
- package/src/controllers/mcp.controller.ts +42 -0
- package/src/decorators/index.ts +1 -0
- package/src/decorators/mcp-tool.decorator.ts +57 -0
- package/src/execution/context-manager.ts +44 -0
- package/src/execution/index.ts +3 -0
- package/src/execution/result-formatter.ts +104 -0
- package/src/execution/tool-executor.ts +61 -0
- package/src/index.ts +5 -0
- package/src/observers/index.ts +1 -0
- package/src/observers/mcp-boot.observer.ts +19 -0
- package/src/protocol/index.ts +3 -0
- package/src/protocol/json-rpc.handler.ts +77 -0
- package/src/protocol/message-router.ts +125 -0
- package/src/protocol/types.ts +309 -0
- package/src/registry/index.ts +3 -0
- package/src/registry/metadata-scanner.ts +105 -0
- package/src/registry/schema-generator.ts +82 -0
- package/src/registry/tool-registry.ts +57 -0
- package/src/transport/index.ts +1 -0
- package/src/transport/sse.transport.ts +56 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {bind, BindingScope} from '@loopback/core';
|
|
2
|
+
import {
|
|
3
|
+
JsonRpcRequest,
|
|
4
|
+
JsonRpcResponse,
|
|
5
|
+
JsonRpcError,
|
|
6
|
+
McpErrorCode,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
@bind({scope: BindingScope.SINGLETON})
|
|
10
|
+
export class JsonRpcHandler {
|
|
11
|
+
createResponse(id: string | number, result: unknown): JsonRpcResponse {
|
|
12
|
+
return {
|
|
13
|
+
jsonrpc: '2.0',
|
|
14
|
+
id,
|
|
15
|
+
result,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createErrorResponse(
|
|
20
|
+
id: string | number | undefined,
|
|
21
|
+
code: McpErrorCode,
|
|
22
|
+
message: string,
|
|
23
|
+
data?: unknown,
|
|
24
|
+
): JsonRpcResponse {
|
|
25
|
+
return {
|
|
26
|
+
jsonrpc: '2.0',
|
|
27
|
+
id: id ?? (null as any),
|
|
28
|
+
error: {
|
|
29
|
+
code,
|
|
30
|
+
message,
|
|
31
|
+
data,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validateRequest(request: unknown): JsonRpcRequest {
|
|
37
|
+
if (!this.isValidRequest(request)) {
|
|
38
|
+
throw this.createError(
|
|
39
|
+
McpErrorCode.INVALID_REQUEST,
|
|
40
|
+
'Invalid JSON-RPC request',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return request as JsonRpcRequest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private isValidRequest(request: unknown): boolean {
|
|
47
|
+
if (typeof request !== 'object' || request === null) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const req = request as any;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
req.jsonrpc === '2.0' &&
|
|
55
|
+
typeof req.method === 'string' &&
|
|
56
|
+
(req.id === undefined ||
|
|
57
|
+
typeof req.id === 'string' ||
|
|
58
|
+
typeof req.id === 'number')
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
createError(
|
|
63
|
+
code: McpErrorCode,
|
|
64
|
+
message: string,
|
|
65
|
+
data?: unknown,
|
|
66
|
+
): JsonRpcError {
|
|
67
|
+
return {
|
|
68
|
+
code,
|
|
69
|
+
message,
|
|
70
|
+
data,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
isNotification(request: JsonRpcRequest): boolean {
|
|
75
|
+
return request.id === undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {bind, BindingScope, inject} from '@loopback/core';
|
|
2
|
+
import {
|
|
3
|
+
JsonRpcRequest,
|
|
4
|
+
JsonRpcResponse,
|
|
5
|
+
McpMethod,
|
|
6
|
+
McpErrorCode,
|
|
7
|
+
InitializeRequest,
|
|
8
|
+
InitializeResult,
|
|
9
|
+
ToolsListRequest,
|
|
10
|
+
ToolsListResult,
|
|
11
|
+
ToolCallRequest,
|
|
12
|
+
ToolCallResult,
|
|
13
|
+
} from './types';
|
|
14
|
+
import {JsonRpcHandler} from './json-rpc.handler';
|
|
15
|
+
import {ToolRegistry} from '../registry/tool-registry';
|
|
16
|
+
import {ToolExecutor} from '../execution/tool-executor';
|
|
17
|
+
|
|
18
|
+
@bind({scope: BindingScope.SINGLETON})
|
|
19
|
+
export class MessageRouter {
|
|
20
|
+
private initialized = false;
|
|
21
|
+
private serverInfo = {
|
|
22
|
+
name: 'LoopBack MCP Server',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
@inject('mcp.jsonRpcHandler')
|
|
28
|
+
private jsonRpcHandler: JsonRpcHandler,
|
|
29
|
+
@inject('mcp.toolRegistry')
|
|
30
|
+
private toolRegistry: ToolRegistry,
|
|
31
|
+
@inject('mcp.toolExecutor')
|
|
32
|
+
private toolExecutor: ToolExecutor,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
async route(request: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
36
|
+
try {
|
|
37
|
+
this.jsonRpcHandler.validateRequest(request);
|
|
38
|
+
|
|
39
|
+
// Handle initialization separately
|
|
40
|
+
if (request.method === McpMethod.INITIALIZE) {
|
|
41
|
+
return await this.handleInitialize(request as InitializeRequest);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Route to appropriate handler
|
|
45
|
+
switch (request.method) {
|
|
46
|
+
case McpMethod.TOOLS_LIST:
|
|
47
|
+
return await this.handleToolsList(request as ToolsListRequest);
|
|
48
|
+
|
|
49
|
+
case McpMethod.TOOLS_CALL:
|
|
50
|
+
return await this.handleToolCall(request as ToolCallRequest);
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
return this.jsonRpcHandler.createErrorResponse(
|
|
54
|
+
request.id!,
|
|
55
|
+
McpErrorCode.METHOD_NOT_FOUND,
|
|
56
|
+
`Method '${request.method}' not found`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return this.handleError(request.id, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async handleInitialize(
|
|
65
|
+
request: InitializeRequest,
|
|
66
|
+
): Promise<JsonRpcResponse> {
|
|
67
|
+
const result: InitializeResult = {
|
|
68
|
+
protocolVersion: '2024-11-05',
|
|
69
|
+
capabilities: {
|
|
70
|
+
tools: {
|
|
71
|
+
listChanged: true,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
serverInfo: this.serverInfo,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.initialized = true;
|
|
78
|
+
|
|
79
|
+
return this.jsonRpcHandler.createResponse(request.id!, result);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async handleToolsList(
|
|
83
|
+
request: ToolsListRequest,
|
|
84
|
+
): Promise<JsonRpcResponse> {
|
|
85
|
+
const tools = this.toolRegistry.list();
|
|
86
|
+
|
|
87
|
+
const result: ToolsListResult = {
|
|
88
|
+
tools,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return this.jsonRpcHandler.createResponse(request.id!, result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async handleToolCall(
|
|
95
|
+
request: ToolCallRequest,
|
|
96
|
+
): Promise<JsonRpcResponse> {
|
|
97
|
+
const {name, arguments: args} = request.params;
|
|
98
|
+
|
|
99
|
+
const result = await this.toolExecutor.execute(name, args || {});
|
|
100
|
+
|
|
101
|
+
return this.jsonRpcHandler.createResponse(request.id!, result);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private handleError(
|
|
105
|
+
id: string | number | undefined,
|
|
106
|
+
error: any,
|
|
107
|
+
): JsonRpcResponse {
|
|
108
|
+
let code = McpErrorCode.INTERNAL_ERROR;
|
|
109
|
+
let message = 'Internal server error';
|
|
110
|
+
let data: unknown;
|
|
111
|
+
|
|
112
|
+
if (error.code && typeof error.code === 'number') {
|
|
113
|
+
code = error.code;
|
|
114
|
+
message = error.message || message;
|
|
115
|
+
data = error.data;
|
|
116
|
+
} else if (error instanceof Error) {
|
|
117
|
+
message = error.message;
|
|
118
|
+
data = {
|
|
119
|
+
stack: error.stack,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return this.jsonRpcHandler.createErrorResponse(id, code, message, data);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// JSON-RPC 2.0 Base Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export interface JsonRpcRequest {
|
|
6
|
+
jsonrpc: '2.0';
|
|
7
|
+
id?: string | number;
|
|
8
|
+
method: string;
|
|
9
|
+
params?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface JsonRpcResponse {
|
|
13
|
+
jsonrpc: '2.0';
|
|
14
|
+
id: string | number;
|
|
15
|
+
result?: unknown;
|
|
16
|
+
error?: JsonRpcError;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface JsonRpcError {
|
|
20
|
+
code: number;
|
|
21
|
+
message: string;
|
|
22
|
+
data?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface JsonRpcNotification {
|
|
26
|
+
jsonrpc: '2.0';
|
|
27
|
+
method: string;
|
|
28
|
+
params?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// MCP Protocol Messages
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export enum McpMethod {
|
|
36
|
+
// Initialization
|
|
37
|
+
INITIALIZE = 'initialize',
|
|
38
|
+
INITIALIZED = 'initialized',
|
|
39
|
+
|
|
40
|
+
// Tools
|
|
41
|
+
TOOLS_LIST = 'tools/list',
|
|
42
|
+
TOOLS_CALL = 'tools/call',
|
|
43
|
+
|
|
44
|
+
// Resources
|
|
45
|
+
RESOURCES_LIST = 'resources/list',
|
|
46
|
+
RESOURCES_READ = 'resources/read',
|
|
47
|
+
RESOURCES_SUBSCRIBE = 'resources/subscribe',
|
|
48
|
+
RESOURCES_UNSUBSCRIBE = 'resources/unsubscribe',
|
|
49
|
+
|
|
50
|
+
// Prompts
|
|
51
|
+
PROMPTS_LIST = 'prompts/list',
|
|
52
|
+
PROMPTS_GET = 'prompts/get',
|
|
53
|
+
|
|
54
|
+
// Logging
|
|
55
|
+
LOGGING_SET_LEVEL = 'logging/setLevel',
|
|
56
|
+
|
|
57
|
+
// Notifications
|
|
58
|
+
NOTIFICATION_CANCELLED = 'notifications/cancelled',
|
|
59
|
+
NOTIFICATION_PROGRESS = 'notifications/progress',
|
|
60
|
+
NOTIFICATION_MESSAGE = 'notifications/message',
|
|
61
|
+
NOTIFICATION_RESOURCES_UPDATED = 'notifications/resources/updated',
|
|
62
|
+
NOTIFICATION_RESOURCES_LIST_CHANGED = 'notifications/resources/list_changed',
|
|
63
|
+
NOTIFICATION_TOOLS_LIST_CHANGED = 'notifications/tools/list_changed',
|
|
64
|
+
NOTIFICATION_PROMPTS_LIST_CHANGED = 'notifications/prompts/list_changed',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Initialize
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export interface InitializeRequest extends JsonRpcRequest {
|
|
72
|
+
method: McpMethod.INITIALIZE;
|
|
73
|
+
params: {
|
|
74
|
+
protocolVersion: string;
|
|
75
|
+
capabilities: ClientCapabilities;
|
|
76
|
+
clientInfo: Implementation;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface InitializeResult {
|
|
81
|
+
protocolVersion: string;
|
|
82
|
+
capabilities: ServerCapabilities;
|
|
83
|
+
serverInfo: Implementation;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Implementation {
|
|
87
|
+
name: string;
|
|
88
|
+
version: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ClientCapabilities {
|
|
92
|
+
experimental?: Record<string, unknown>;
|
|
93
|
+
sampling?: Record<string, unknown>;
|
|
94
|
+
roots?: {
|
|
95
|
+
listChanged?: boolean;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ServerCapabilities {
|
|
100
|
+
experimental?: Record<string, unknown>;
|
|
101
|
+
logging?: Record<string, unknown>;
|
|
102
|
+
prompts?: {
|
|
103
|
+
listChanged?: boolean;
|
|
104
|
+
};
|
|
105
|
+
resources?: {
|
|
106
|
+
subscribe?: boolean;
|
|
107
|
+
listChanged?: boolean;
|
|
108
|
+
};
|
|
109
|
+
tools?: {
|
|
110
|
+
listChanged?: boolean;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Tools
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
export interface Tool {
|
|
119
|
+
name: string;
|
|
120
|
+
description?: string;
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object';
|
|
123
|
+
properties?: Record<string, JsonSchema>;
|
|
124
|
+
required?: string[];
|
|
125
|
+
additionalProperties?: boolean;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ToolsListRequest extends JsonRpcRequest {
|
|
130
|
+
method: McpMethod.TOOLS_LIST;
|
|
131
|
+
params?: {
|
|
132
|
+
cursor?: string;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ToolsListResult {
|
|
137
|
+
tools: Tool[];
|
|
138
|
+
nextCursor?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ToolCallRequest extends JsonRpcRequest {
|
|
142
|
+
method: McpMethod.TOOLS_CALL;
|
|
143
|
+
params: {
|
|
144
|
+
name: string;
|
|
145
|
+
arguments?: Record<string, unknown>;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ToolCallResult {
|
|
150
|
+
content: Content[];
|
|
151
|
+
isError?: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Content Types
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
export type Content = TextContent | ImageContent | EmbeddedResource;
|
|
159
|
+
|
|
160
|
+
export interface TextContent {
|
|
161
|
+
type: 'text';
|
|
162
|
+
text: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ImageContent {
|
|
166
|
+
type: 'image';
|
|
167
|
+
data: string; // base64
|
|
168
|
+
mimeType: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface EmbeddedResource {
|
|
172
|
+
type: 'resource';
|
|
173
|
+
resource: ResourceContents;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Resources
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
export interface Resource {
|
|
181
|
+
uri: string;
|
|
182
|
+
name: string;
|
|
183
|
+
description?: string;
|
|
184
|
+
mimeType?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface ResourcesListRequest extends JsonRpcRequest {
|
|
188
|
+
method: McpMethod.RESOURCES_LIST;
|
|
189
|
+
params?: {
|
|
190
|
+
cursor?: string;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ResourcesListResult {
|
|
195
|
+
resources: Resource[];
|
|
196
|
+
nextCursor?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ResourceReadRequest extends JsonRpcRequest {
|
|
200
|
+
method: McpMethod.RESOURCES_READ;
|
|
201
|
+
params: {
|
|
202
|
+
uri: string;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ResourceContents {
|
|
207
|
+
uri: string;
|
|
208
|
+
mimeType?: string;
|
|
209
|
+
text?: string;
|
|
210
|
+
blob?: string; // base64
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface ResourceReadResult {
|
|
214
|
+
contents: ResourceContents[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Prompts
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
export interface Prompt {
|
|
222
|
+
name: string;
|
|
223
|
+
description?: string;
|
|
224
|
+
arguments?: PromptArgument[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface PromptArgument {
|
|
228
|
+
name: string;
|
|
229
|
+
description?: string;
|
|
230
|
+
required?: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface PromptsListRequest extends JsonRpcRequest {
|
|
234
|
+
method: McpMethod.PROMPTS_LIST;
|
|
235
|
+
params?: {
|
|
236
|
+
cursor?: string;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface PromptsListResult {
|
|
241
|
+
prompts: Prompt[];
|
|
242
|
+
nextCursor?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface PromptGetRequest extends JsonRpcRequest {
|
|
246
|
+
method: McpMethod.PROMPTS_GET;
|
|
247
|
+
params: {
|
|
248
|
+
name: string;
|
|
249
|
+
arguments?: Record<string, string>;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface PromptMessage {
|
|
254
|
+
role: 'user' | 'assistant';
|
|
255
|
+
content: Content;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface PromptGetResult {
|
|
259
|
+
description?: string;
|
|
260
|
+
messages: PromptMessage[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// JSON Schema
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
export interface JsonSchema {
|
|
268
|
+
type?: string | string[];
|
|
269
|
+
properties?: Record<string, JsonSchema>;
|
|
270
|
+
items?: JsonSchema | JsonSchema[];
|
|
271
|
+
required?: string[];
|
|
272
|
+
enum?: unknown[];
|
|
273
|
+
const?: unknown;
|
|
274
|
+
description?: string;
|
|
275
|
+
default?: unknown;
|
|
276
|
+
examples?: unknown[];
|
|
277
|
+
format?: string;
|
|
278
|
+
pattern?: string;
|
|
279
|
+
minimum?: number;
|
|
280
|
+
maximum?: number;
|
|
281
|
+
minLength?: number;
|
|
282
|
+
maxLength?: number;
|
|
283
|
+
minItems?: number;
|
|
284
|
+
maxItems?: number;
|
|
285
|
+
uniqueItems?: boolean;
|
|
286
|
+
additionalProperties?: boolean | JsonSchema;
|
|
287
|
+
[key: string]: unknown;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// Error Codes
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
export enum McpErrorCode {
|
|
295
|
+
// Standard JSON-RPC errors
|
|
296
|
+
PARSE_ERROR = -32700,
|
|
297
|
+
INVALID_REQUEST = -32600,
|
|
298
|
+
METHOD_NOT_FOUND = -32601,
|
|
299
|
+
INVALID_PARAMS = -32602,
|
|
300
|
+
INTERNAL_ERROR = -32603,
|
|
301
|
+
|
|
302
|
+
// MCP-specific errors
|
|
303
|
+
TOOL_NOT_FOUND = -32001,
|
|
304
|
+
TOOL_EXECUTION_ERROR = -32002,
|
|
305
|
+
RESOURCE_NOT_FOUND = -32003,
|
|
306
|
+
RESOURCE_ACCESS_DENIED = -32004,
|
|
307
|
+
PROMPT_NOT_FOUND = -32005,
|
|
308
|
+
INVALID_TOOL_ARGUMENTS = -32006,
|
|
309
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {bind, BindingScope, inject, Application} from '@loopback/core';
|
|
2
|
+
import {ToolRegistry, ToolDescriptor} from './tool-registry';
|
|
3
|
+
import {McpToolMetadata, MCP_TOOL_METADATA_KEY} from '../decorators';
|
|
4
|
+
import {SchemaGenerator} from './schema-generator';
|
|
5
|
+
import {MetadataInspector} from '@loopback/core';
|
|
6
|
+
|
|
7
|
+
@bind({scope: BindingScope.SINGLETON})
|
|
8
|
+
export class MetadataScanner {
|
|
9
|
+
constructor(
|
|
10
|
+
@inject('mcp.toolRegistry')
|
|
11
|
+
private toolRegistry: ToolRegistry,
|
|
12
|
+
@inject('mcp.schemaGenerator')
|
|
13
|
+
private schemaGenerator: SchemaGenerator,
|
|
14
|
+
@inject.context()
|
|
15
|
+
private app: Application,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async scan(): Promise<void> {
|
|
19
|
+
const controllerBindings = this.app.findByTag('controller');
|
|
20
|
+
for (const binding of controllerBindings) {
|
|
21
|
+
const controllerClass = binding.valueConstructor;
|
|
22
|
+
if (controllerClass) {
|
|
23
|
+
await this.scanControllerClass(controllerClass);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async scanControllerClass(controllerClass: Function): Promise<void> {
|
|
29
|
+
try {
|
|
30
|
+
const proto = controllerClass.prototype;
|
|
31
|
+
const methodNames = Object.getOwnPropertyNames(proto).filter(
|
|
32
|
+
name => typeof proto[name] === 'function' && name !== 'constructor',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
for (const methodName of methodNames) {
|
|
36
|
+
const metadata = MetadataInspector.getMethodMetadata<McpToolMetadata>(
|
|
37
|
+
MCP_TOOL_METADATA_KEY,
|
|
38
|
+
proto,
|
|
39
|
+
methodName,
|
|
40
|
+
);
|
|
41
|
+
if (metadata) {
|
|
42
|
+
await this.registerTool(controllerClass, methodName, metadata);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(`[MCP] Skipping controller during scan:`, error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async registerTool(
|
|
51
|
+
controllerClass: Function,
|
|
52
|
+
methodName: string,
|
|
53
|
+
metadata: McpToolMetadata,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
// Schema: use LoopBack's own spec generation (resolves $ref, always correct)
|
|
56
|
+
const inputSchema = metadata.inputSchema ?? this.schemaGenerator.generateForMethod(controllerClass, methodName);
|
|
57
|
+
|
|
58
|
+
// Handler arg mapping: read raw LoopBack parameter metadata set at class-definition time
|
|
59
|
+
const proto = controllerClass.prototype;
|
|
60
|
+
const allParamsMeta: Record<string, Array<{name: string; in: string} | null>> =
|
|
61
|
+
Reflect.getMetadata('loopback:openapi-v3:parameters', proto) ?? {};
|
|
62
|
+
const allReqBodyMeta: Record<string, Array<object | null>> =
|
|
63
|
+
Reflect.getMetadata('loopback:openapi-v3:request-body', proto) ?? {};
|
|
64
|
+
|
|
65
|
+
const methodParams = allParamsMeta[methodName] ?? [];
|
|
66
|
+
const methodReqBodies = allReqBodyMeta[methodName] ?? [];
|
|
67
|
+
const maxArgs = Math.max(methodParams.length, methodReqBodies.length);
|
|
68
|
+
|
|
69
|
+
const descriptor: ToolDescriptor = {
|
|
70
|
+
name: metadata.name,
|
|
71
|
+
description: metadata.description,
|
|
72
|
+
inputSchema,
|
|
73
|
+
controller: proto,
|
|
74
|
+
methodName,
|
|
75
|
+
handler: async (args: Record<string, unknown>) => {
|
|
76
|
+
const controller = await this.app.get(`controllers.${controllerClass.name}`);
|
|
77
|
+
|
|
78
|
+
if (maxArgs === 0) {
|
|
79
|
+
return (controller as any)[methodName](args);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const positionalArgs: unknown[] = [];
|
|
83
|
+
const usedKeys = new Set<string>();
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < maxArgs; i++) {
|
|
86
|
+
const param = methodParams[i];
|
|
87
|
+
const reqBody = methodReqBodies[i];
|
|
88
|
+
|
|
89
|
+
if (param) {
|
|
90
|
+
positionalArgs[i] = args[param.name];
|
|
91
|
+
usedKeys.add(param.name);
|
|
92
|
+
} else if (reqBody) {
|
|
93
|
+
positionalArgs[i] = Object.fromEntries(
|
|
94
|
+
Object.entries(args).filter(([k]) => !usedKeys.has(k)),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (controller as any)[methodName](...positionalArgs);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
this.toolRegistry.register(descriptor);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {bind, BindingScope} from '@loopback/core';
|
|
2
|
+
import {getControllerSpec} from '@loopback/openapi-v3';
|
|
3
|
+
import {JsonSchema} from '../protocol/types';
|
|
4
|
+
|
|
5
|
+
@bind({scope: BindingScope.SINGLETON})
|
|
6
|
+
export class SchemaGenerator {
|
|
7
|
+
/**
|
|
8
|
+
* Build the MCP inputSchema for a controller method using LoopBack's
|
|
9
|
+
* own getControllerSpec(), so $ref resolution and parameter ordering
|
|
10
|
+
* are always correct regardless of decorator evaluation timing.
|
|
11
|
+
*
|
|
12
|
+
* Path params become required top-level properties.
|
|
13
|
+
* Request body properties are spread into the top-level object.
|
|
14
|
+
* Optional query params (filter, where…) are omitted — MCP tools
|
|
15
|
+
* accept them as part of the body object when passed.
|
|
16
|
+
*/
|
|
17
|
+
generateForMethod(controllerClass: Function, methodName: string): JsonSchema {
|
|
18
|
+
const spec = getControllerSpec(controllerClass as any);
|
|
19
|
+
const components: Record<string, any> = spec.components?.schemas ?? {};
|
|
20
|
+
|
|
21
|
+
const operation = this.findOperation(spec, methodName);
|
|
22
|
+
if (!operation) {
|
|
23
|
+
return {type: 'object', properties: {}, additionalProperties: false};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const properties: Record<string, JsonSchema> = {};
|
|
27
|
+
const required: string[] = [];
|
|
28
|
+
const usedKeys = new Set<string>();
|
|
29
|
+
|
|
30
|
+
// Path parameters (required, strongly typed)
|
|
31
|
+
for (const param of (operation.parameters ?? []) as any[]) {
|
|
32
|
+
if (param.in !== 'path') continue;
|
|
33
|
+
const schema = this.resolveRef(param.schema ?? {type: 'string'}, components);
|
|
34
|
+
properties[param.name] = schema;
|
|
35
|
+
usedKeys.add(param.name);
|
|
36
|
+
required.push(param.name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Request body: spread its resolved properties into the top-level schema
|
|
40
|
+
const bodyContent = operation.requestBody?.content?.['application/json'];
|
|
41
|
+
if (bodyContent?.schema) {
|
|
42
|
+
const bodySchema = this.resolveRef(bodyContent.schema, components);
|
|
43
|
+
if (bodySchema.properties) {
|
|
44
|
+
for (const [key, val] of Object.entries(bodySchema.properties)) {
|
|
45
|
+
if (!usedKeys.has(key)) {
|
|
46
|
+
properties[key] = this.resolveRef(val as any, components);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const r of bodySchema.required ?? []) {
|
|
50
|
+
if (!required.includes(r)) required.push(r);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties,
|
|
58
|
+
required: required.length > 0 ? required : undefined,
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Resolve a $ref schema from components.schemas, or return it as-is. */
|
|
64
|
+
private resolveRef(schema: any, components: Record<string, any>): any {
|
|
65
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
66
|
+
if (!schema['$ref']) return schema;
|
|
67
|
+
const refName = (schema['$ref'] as string).split('/').pop()!;
|
|
68
|
+
return components[refName] ?? schema;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Find the OpenAPI operation matching a controller method by x-operation-name. */
|
|
72
|
+
private findOperation(spec: any, methodName: string): any {
|
|
73
|
+
for (const pathItem of Object.values(spec.paths ?? {})) {
|
|
74
|
+
for (const op of Object.values(pathItem as object)) {
|
|
75
|
+
if (op && (op as any)['x-operation-name'] === methodName) {
|
|
76
|
+
return op;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|