Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/run-scenarios-helpers](./build-tests/run-scenarios-helpers/) | Helpers for the *-scenarios test projects. |
| [/build-tests/rush-amazon-s3-build-cache-plugin-integration-test](./build-tests/rush-amazon-s3-build-cache-plugin-integration-test/) | Tests connecting to an amazon S3 endpoint |
| [/build-tests/rush-lib-declaration-paths-test](./build-tests/rush-lib-declaration-paths-test/) | This project ensures all of the paths in rush-lib/lib/... have imports that resolve correctly. If this project builds, all `lib/**/*.d.ts` files in the `@microsoft/rush-lib` package are valid. |
| [/build-tests/rush-mcp-example-plugin](./build-tests/rush-mcp-example-plugin/) | Example showing how to create a plugin for @rushstack/mcp-server |
| [/build-tests/rush-project-change-analyzer-test](./build-tests/rush-project-change-analyzer-test/) | This is an example project that uses rush-lib's ProjectChangeAnalyzer to |
| [/build-tests/rush-redis-cobuild-plugin-integration-test](./build-tests/rush-redis-cobuild-plugin-integration-test/) | Tests connecting to an redis server |
| [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin |
Expand Down
19 changes: 19 additions & 0 deletions apps/rush-mcp-server/config/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",

"mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",

"apiReport": {
"enabled": true,
"reportFolder": "../../../common/reviews/api"
},

"docModel": {
"enabled": true,
"apiJsonFilePath": "../../../common/temp/api/<unscopedPackageName>.api.json"
},

"dtsRollup": {
"enabled": true
}
}
2 changes: 2 additions & 0 deletions apps/rush-mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"monorepo",
"server"
],
"main": "lib/index.js",
"typings": "dist/mcp-server.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/rushstack.git",
Expand Down
11 changes: 9 additions & 2 deletions apps/rush-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

export { log } from './utilities/log';
export * from './tools';
/**
* API for use by MCP plugins.
* @packageDocumentation
*/

export * from './pluginFramework/IRushMcpPlugin';
export * from './pluginFramework/IRushMcpTool';
export { type IRegisterToolOptions, RushMcpPluginSession } from './pluginFramework/RushMcpPluginSession';
export * from './pluginFramework/zodTypes';
22 changes: 22 additions & 0 deletions apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { RushMcpPluginSession } from './RushMcpPluginSession';

/**
* MCP plugins should implement this interface.
* @public
*/
export interface IRushMcpPlugin {
onInitializeAsync(): Promise<void>;
}

/**
* The plugin's entry point should return this function as its default export.
* @public
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type RushMcpPluginFactory<TConfigFile = {}> = (
session: RushMcpPluginSession,
configFile: TConfigFile | undefined
) => IRushMcpPlugin;
17 changes: 17 additions & 0 deletions apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type * as zod from 'zod';

import type { CallToolResult } from './zodTypes';

/**
* MCP plugins should implement this interface.
* @public
*/
export interface IRushMcpTool<
TSchema extends zod.ZodObject<zod.ZodRawShape> = zod.ZodObject<zod.ZodRawShape>
> {
readonly schema: TSchema;
executeAsync(input: zod.infer<TSchema>): Promise<CallToolResult>;
}
193 changes: 193 additions & 0 deletions apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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, type JsonObject, 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 { McpServer } from '@modelcontextprotocol/sdk/server/mcp';

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;

/**
* The name of the plugin. This is used to identify the plugin in the MCP server.
*/
pluginName: 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;
private readonly _mcpServer: McpServer;

public constructor(rushWorkspacePath: string, mcpServer: McpServer) {
this._rushWorkspacePath = rushWorkspacePath;
this._mcpServer = mcpServer;
}

public async loadAsync(): Promise<void> {
const rushMcpFilePath: string = path.join(
this._rushWorkspacePath,
'common/config/rush-mcp/rush-mcp.json'
);

if (!(await FileSystem.existsAsync(rushMcpFilePath))) {
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
);

let rushMcpPluginOptions: JsonObject = {};
if (jsonManifest.configFileSchema) {
const mcpPluginSchemaFilePath: string = path.resolve(
installedPluginPackageFolder,
jsonManifest.configFileSchema
);
const mcpPluginSchema: JsonSchema = await JsonSchema.fromFile(mcpPluginSchemaFilePath);
const rushMcpPluginOptionsFilePath: string = path.resolve(
this._rushWorkspacePath,
`common/config/rush-mcp/${jsonMcpPlugin.pluginName}.json`
);
// Example: /path/to/my-repo/common/config/rush-mcp/rush-mcp-example-plugin.json
rushMcpPluginOptions = await JsonFile.loadAndValidateAsync(
rushMcpPluginOptionsFilePath,
mcpPluginSchema
);
}

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(this._mcpServer);

let plugin: IRushMcpPlugin;
try {
plugin = pluginFactory(session, rushMcpPluginOptions);
} 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()
);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as zod from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp';

import type { IRushMcpTool } from './IRushMcpTool';
import type { zodModule } from './zodTypes';

/**
* Each plugin gets its own session.
*
* @public
*/
export interface IRegisterToolOptions {
toolName: string;
description?: string;
}

/**
* Each plugin gets its own session.
*
* @public
*/
export abstract class RushMcpPluginSession {
public readonly zod: typeof zodModule = zod;
public abstract registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void;
}

export class RushMcpPluginSessionInternal extends RushMcpPluginSession {
private readonly _mcpServer: McpServer;

public constructor(mcpServer: McpServer) {
super();
this._mcpServer = mcpServer;
}

public override registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void {
if (options.description) {
this._mcpServer.tool(
options.toolName,
options.description,
tool.schema.shape,
tool.executeAsync.bind(tool)
);
} else {
this._mcpServer.tool(options.toolName, tool.schema.shape, tool.executeAsync.bind(tool));
}
}
}
14 changes: 14 additions & 0 deletions apps/rush-mcp-server/src/pluginFramework/zodTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type * as zod from 'zod';
export type { zod as zodModule };

import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types';

export { CallToolResultSchema };

/**
* @public
*/
export type CallToolResult = zod.infer<typeof CallToolResultSchema>;
21 changes: 21 additions & 0 deletions apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Rush MCP Plugin Manifest",
"type": "object",
"properties": {
"pluginName": {
"type": "string",
"description": "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."
},
"configFileSchema": {
"type": "string",
"description": "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`."
},
"entryPoint": {
"type": "string",
"description": "The module path to the plugin's entry point. Its default export must be a class implementing the MCP plugin interface."
}
},
"required": ["pluginName", "entryPoint"],
"additionalProperties": false
}
Loading