forked from microsoft/rushstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRushMcpPluginLoader.ts
More file actions
171 lines (143 loc) · 6.05 KB
/
RushMcpPluginLoader.ts
File metadata and controls
171 lines (143 loc) · 6.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'path';
import { FileSystem, Import, JsonFile, JsonSchema } from '@rushstack/node-core-library';
import { Autoinstaller } from '@rushstack/rush-sdk/lib/logic/Autoinstaller';
import { RushGlobalFolder } from '@rushstack/rush-sdk/lib/api/RushGlobalFolder';
import { RushConfiguration } from '@rushstack/rush-sdk/lib/api/RushConfiguration';
import type { IRushMcpPlugin, RushMcpPluginFactory } from './IRushMcpPlugin';
import { RushMcpPluginSessionInternal } from './RushMcpPluginSession';
import rushMcpJsonSchemaObject from '../schemas/rush-mcp.schema.json';
import rushMcpPluginSchemaObject from '../schemas/rush-mcp-plugin.schema.json';
/**
* Configuration for @rushstack/mcp-server in a monorepo.
* Corresponds to the contents of common/config/rush-mcp/rush-mcp.json
*/
export interface IJsonRushMcpConfig {
/**
* The list of plugins that @rushstack/mcp-server should load when processing this monorepo.
*/
mcpPlugins: IJsonRushMcpPlugin[];
}
/**
* Describes a single MCP plugin entry.
*/
export interface IJsonRushMcpPlugin {
/**
* The name of an NPM package that appears in the package.json "dependencies" for the autoinstaller.
*/
packageName: string;
/**
* The name of a Rush autoinstaller with this package as its dependency.
* @rushstack/mcp-server will ensure this folder is installed before loading the plugin.
*/
autoinstaller: string;
}
/**
* Manifest file for a Rush MCP plugin.
* Every plugin package must contain a "rush-mcp-plugin.json" manifest in the top-level folder.
*/
export interface IJsonRushMcpPluginManifest {
/**
* A name that uniquely identifies your plugin.
* Generally this should match the NPM package name; two plugins with the same pluginName cannot be loaded together.
*/
pluginName: string;
/**
* Optional. Indicates that your plugin accepts a config file.
* The MCP server will load this schema file and provide it to the plugin.
* Path is typically `<rush-repo>/common/config/rush-mcp/<plugin-name>.json`.
*/
configFileSchema?: string;
/**
* The module path to the plugin's entry point.
* Its default export must be a class implementing the MCP plugin interface.
*/
entryPoint: string;
}
export class RushMcpPluginLoader {
private static readonly _rushMcpJsonSchema: JsonSchema =
JsonSchema.fromLoadedObject(rushMcpJsonSchemaObject);
private static readonly _rushMcpPluginSchemaObject: JsonSchema =
JsonSchema.fromLoadedObject(rushMcpPluginSchemaObject);
private readonly _rushWorkspacePath: string;
public constructor(rushWorkspacePath: string) {
this._rushWorkspacePath = rushWorkspacePath;
}
public async loadAsync(): Promise<void> {
const rushMcpFilePath: string = path.join(
this._rushWorkspacePath,
'common/config/rush-mcp/rush-mcp.json'
);
if (!(await FileSystem.existsAsync(rushMcpFilePath))) {
// Should we report an error here?
return;
}
const rushConfiguration: RushConfiguration = RushConfiguration.loadFromDefaultLocation({
startingFolder: this._rushWorkspacePath
});
const jsonRushMcpConfig: IJsonRushMcpConfig = await JsonFile.loadAndValidateAsync(
rushMcpFilePath,
RushMcpPluginLoader._rushMcpJsonSchema
);
if (jsonRushMcpConfig.mcpPlugins.length === 0) {
return;
}
const rushGlobalFolder: RushGlobalFolder = new RushGlobalFolder();
for (const jsonMcpPlugin of jsonRushMcpConfig.mcpPlugins) {
// Ensure the autoinstaller is installed
const autoinstaller: Autoinstaller = new Autoinstaller({
autoinstallerName: jsonMcpPlugin.autoinstaller,
rushConfiguration,
rushGlobalFolder,
restrictConsoleOutput: false
});
await autoinstaller.prepareAsync();
// Load the manifest
// Suppose the autoinstaller is "my-autoinstaller" and the package is "rush-mcp-example-plugin".
// Then the folder will be:
// "/path/to/my-repo/common/autoinstallers/my-autoinstaller/node_modules/rush-mcp-example-plugin"
const installedPluginPackageFolder: string = await Import.resolvePackageAsync({
baseFolderPath: autoinstaller.folderFullPath,
packageName: jsonMcpPlugin.packageName
});
const manifestFilePath: string = path.join(installedPluginPackageFolder, 'rush-mcp-plugin.json');
if (!(await FileSystem.existsAsync(manifestFilePath))) {
throw new Error(
'The "rush-mcp-plugin.json" manifest file was not found under ' + installedPluginPackageFolder
);
}
const jsonManifest: IJsonRushMcpPluginManifest = await JsonFile.loadAndValidateAsync(
manifestFilePath,
RushMcpPluginLoader._rushMcpPluginSchemaObject
);
// TODO: Load and validate config file if defined by the manifest
const fullEntryPointPath: string = path.join(installedPluginPackageFolder, jsonManifest.entryPoint);
let pluginFactory: RushMcpPluginFactory;
try {
const entryPointModule: { default?: RushMcpPluginFactory } = require(fullEntryPointPath);
if (entryPointModule.default === undefined) {
throw new Error('The commonJS "default" export is missing');
}
pluginFactory = entryPointModule.default;
} catch (e) {
throw new Error(`Unable to load plugin entry point at ${fullEntryPointPath}: ` + e.toString());
}
const session: RushMcpPluginSessionInternal = new RushMcpPluginSessionInternal();
let plugin: IRushMcpPlugin;
try {
// TODO: Replace "{}" with the plugin's parsed config file JSON
plugin = pluginFactory(session, {});
} catch (e) {
throw new Error(`Error invoking entry point for plugin ${jsonManifest.pluginName}:` + e.toString());
}
try {
await plugin.onInitializeAsync();
} catch (e) {
throw new Error(
`Error occurred in onInitializeAsync() for plugin ${jsonManifest.pluginName}:` + e.toString()
);
}
}
}
}