From 6cb84da4a6eab96fc370cdca9355c70efbfde819 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Thu, 1 May 2025 20:22:40 -0700 Subject: [PATCH 1/8] Add a new test project "rush-mcp-example-plugin" --- .../rush-mcp-example-plugin/.eslintrc.js | 13 ++++++++ .../rush-mcp-example-plugin/.npmignore | 32 +++++++++++++++++++ build-tests/rush-mcp-example-plugin/LICENSE | 24 ++++++++++++++ build-tests/rush-mcp-example-plugin/README.md | 3 ++ .../rush-mcp-example-plugin/config/rig.json | 7 ++++ .../rush-mcp-example-plugin/package.json | 18 +++++++++++ .../rush-mcp-example-plugin/src/index.ts | 4 +++ .../rush-mcp-example-plugin/tsconfig.json | 3 ++ .../rush/nonbrowser-approved-packages.json | 4 +++ .../config/subspaces/default/pnpm-lock.yaml | 15 +++++++++ rush.json | 6 ++++ 11 files changed, 129 insertions(+) create mode 100644 build-tests/rush-mcp-example-plugin/.eslintrc.js create mode 100644 build-tests/rush-mcp-example-plugin/.npmignore create mode 100644 build-tests/rush-mcp-example-plugin/LICENSE create mode 100644 build-tests/rush-mcp-example-plugin/README.md create mode 100644 build-tests/rush-mcp-example-plugin/config/rig.json create mode 100644 build-tests/rush-mcp-example-plugin/package.json create mode 100644 build-tests/rush-mcp-example-plugin/src/index.ts create mode 100644 build-tests/rush-mcp-example-plugin/tsconfig.json diff --git a/build-tests/rush-mcp-example-plugin/.eslintrc.js b/build-tests/rush-mcp-example-plugin/.eslintrc.js new file mode 100644 index 00000000000..de794c04ae0 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/.eslintrc.js @@ -0,0 +1,13 @@ +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-eslint-config/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-eslint-config/patch/custom-config-package-names'); + +module.exports = { + extends: [ + 'local-eslint-config/profile/node', + 'local-eslint-config/mixins/friendly-locals', + 'local-eslint-config/mixins/tsdoc' + ], + parserOptions: { tsconfigRootDir: __dirname } +}; diff --git a/build-tests/rush-mcp-example-plugin/.npmignore b/build-tests/rush-mcp-example-plugin/.npmignore new file mode 100644 index 00000000000..bc349f9a4be --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/.npmignore @@ -0,0 +1,32 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- diff --git a/build-tests/rush-mcp-example-plugin/LICENSE b/build-tests/rush-mcp-example-plugin/LICENSE new file mode 100644 index 00000000000..5ad10fc49f8 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/LICENSE @@ -0,0 +1,24 @@ +rush-mcp-example-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build-tests/rush-mcp-example-plugin/README.md b/build-tests/rush-mcp-example-plugin/README.md new file mode 100644 index 00000000000..8ca3190b2fc --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/README.md @@ -0,0 +1,3 @@ +# rush-mcp-example-plugin + +This example project shows how to create a plugin for `@rushstack/mcp-server` diff --git a/build-tests/rush-mcp-example-plugin/config/rig.json b/build-tests/rush-mcp-example-plugin/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "local-node-rig" +} diff --git a/build-tests/rush-mcp-example-plugin/package.json b/build-tests/rush-mcp-example-plugin/package.json new file mode 100644 index 00000000000..2f16f0b0044 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/package.json @@ -0,0 +1,18 @@ +{ + "name": "rush-mcp-example-plugin", + "version": "0.0.0", + "private": true, + "description": "Example showing how to create a plugin for @rushstack/mcp-server", + "license": "MIT", + "scripts": { + "build": "heft build --clean", + "_phase:build": "heft run --only build -- --clean" + }, + "dependencies": {}, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "@rushstack/mcp-server": "workspace:*", + "local-node-rig": "workspace:*", + "local-eslint-config": "workspace:*" + } +} diff --git a/build-tests/rush-mcp-example-plugin/src/index.ts b/build-tests/rush-mcp-example-plugin/src/index.ts new file mode 100644 index 00000000000..54f65999418 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export class X {} diff --git a/build-tests/rush-mcp-example-plugin/tsconfig.json b/build-tests/rush-mcp-example-plugin/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 5127417f3a6..b3ab157559e 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -226,6 +226,10 @@ "name": "@rushstack/lookup-by-path", "allowedCategories": [ "libraries" ] }, + { + "name": "@rushstack/mcp-server", + "allowedCategories": [ "tests" ] + }, { "name": "@rushstack/module-minifier", "allowedCategories": [ "libraries", "tests" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index ecc2d923870..50b289c4c78 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -2249,6 +2249,21 @@ importers: specifier: workspace:* version: link:../../rigs/local-node-rig + ../../../build-tests/rush-mcp-example-plugin: + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/mcp-server': + specifier: workspace:* + version: link:../../apps/rush-mcp-server + local-eslint-config: + specifier: workspace:* + version: link:../../eslint/local-eslint-config + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../build-tests/rush-project-change-analyzer-test: dependencies: '@microsoft/rush-lib': diff --git a/rush.json b/rush.json index 3d8f5018a1e..f1ad0ab70b4 100644 --- a/rush.json +++ b/rush.json @@ -660,6 +660,12 @@ "reviewCategory": "tests", "shouldPublish": false }, + { + "packageName": "rush-mcp-example-plugin", + "projectFolder": "build-tests/rush-mcp-example-plugin", + "reviewCategory": "tests", + "shouldPublish": false + }, { "packageName": "run-scenarios-helpers", "projectFolder": "build-tests/run-scenarios-helpers", From 827442f574bbdf7434184a991b84e1310252d026 Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Thu, 1 May 2025 23:05:17 -0700 Subject: [PATCH 2/8] Initial plugin design --- .../rush-mcp-server/config/api-extractor.json | 19 ++++++++ apps/rush-mcp-server/package.json | 2 + apps/rush-mcp-server/src/index.ts | 11 ++++- .../src/pluginFramework/IRushMcpPlugin.ts | 22 +++++++++ .../src/pluginFramework/IRushMcpTool.ts | 15 ++++++ .../pluginFramework/RushMcpPluginSession.ts | 23 +++++++++ .../src/pluginFramework/zodTypes.ts | 14 ++++++ .../rush-mcp-plugin.json | 24 ++++++++++ .../src/ExamplePlugin.ts | 23 +++++++++ .../src/StateCapitalTool.ts | 42 ++++++++++++++++ .../rush-mcp-example-plugin/src/index.ts | 12 ++++- .../src/rush-mcp-example-plugin.schema.json | 19 ++++++++ common/reviews/api/mcp-server.api.md | 48 +++++++++++++++++++ 13 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 apps/rush-mcp-server/config/api-extractor.json create mode 100644 apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts create mode 100644 apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts create mode 100644 apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts create mode 100644 apps/rush-mcp-server/src/pluginFramework/zodTypes.ts create mode 100644 build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json create mode 100644 build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts create mode 100644 build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts create mode 100644 build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json create mode 100644 common/reviews/api/mcp-server.api.md diff --git a/apps/rush-mcp-server/config/api-extractor.json b/apps/rush-mcp-server/config/api-extractor.json new file mode 100644 index 00000000000..996e271d3dd --- /dev/null +++ b/apps/rush-mcp-server/config/api-extractor.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "../../../common/temp/api/.api.json" + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/apps/rush-mcp-server/package.json b/apps/rush-mcp-server/package.json index 74f028bb1a5..ace131c1d00 100644 --- a/apps/rush-mcp-server/package.json +++ b/apps/rush-mcp-server/package.json @@ -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", diff --git a/apps/rush-mcp-server/src/index.ts b/apps/rush-mcp-server/src/index.ts index 7dca0d0f5e9..8e16a12951e 100644 --- a/apps/rush-mcp-server/src/index.ts +++ b/apps/rush-mcp-server/src/index.ts @@ -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 * from './pluginFramework/RushMcpPluginSession'; +export * from './pluginFramework/zodTypes'; diff --git a/apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts b/apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts new file mode 100644 index 00000000000..fa74680ccab --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/IRushMcpPlugin.ts @@ -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; +} + +/** + * 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 = ( + session: RushMcpPluginSession, + configFile: TConfigFile | undefined +) => IRushMcpPlugin; diff --git a/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts b/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts new file mode 100644 index 00000000000..f6eaba91d70 --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts @@ -0,0 +1,15 @@ +// 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 { + readonly schema: TSchema; + executeAsync(input: zod.infer): Promise; +} diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts new file mode 100644 index 00000000000..567f7ef007f --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IRushMcpTool } from './IRushMcpTool'; + +/** + * Each plugin gets its own session. + * + * @public + */ +export interface IRegisterToolOptions { + toolName: string; + description?: string; +} + +/** + * Each plugin gets its own session. + * + * @public + */ +export class RushMcpPluginSession { + public registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void {} +} diff --git a/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts b/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts new file mode 100644 index 00000000000..a8f7aa6d92f --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts @@ -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 * as zod from 'zod'; +export { zod }; + +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types'; + +export { CallToolResultSchema }; + +/** + * @public + */ +export type CallToolResult = zod.infer; diff --git a/build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json b/build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json new file mode 100644 index 00000000000..48ff8a67fb9 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/rush-mcp-plugin.json @@ -0,0 +1,24 @@ +/** + * Every plugin package must contain a "rush-mcp-plugin.json" manifest in the top-level folder + * (next to package.json). + */ +{ + /** + * A name that uniquely identifies your plugin. Generally this should be the same name as + * the NPM package. If two NPM packages have the same pluginName, they cannot be loaded together. + */ + "pluginName": "rush-mcp-example-plugin", + + /** + * (OPTIONAL) Indicates that your plugin accepts a config file. The MCP server will load this + * file and provide it to the plugin. + * + * The config file path will be `/common/config/rush-mcp/.json`. + */ + "configFileSchema": "./lib/rush-mcp-example-plugin.schema.json", + + /** + * The entry point, whose default export should be a class that implements + */ + "entryPoint": "./lib/index.js" +} diff --git a/build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts b/build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts new file mode 100644 index 00000000000..d706e8c9a52 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/ExamplePlugin.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IRushMcpPlugin, RushMcpPluginSession } from '@rushstack/mcp-server'; +import { StateCapitalTool } from './StateCapitalTool'; + +export interface IExamplePluginConfigFile { + capitalsByState: Record; +} + +export class ExamplePlugin implements IRushMcpPlugin { + public session: RushMcpPluginSession; + public configFile: IExamplePluginConfigFile | undefined = undefined; + + public constructor(session: RushMcpPluginSession, configFile: IExamplePluginConfigFile | undefined) { + this.session = session; + this.configFile = configFile; + } + + public async onInitializeAsync(): Promise { + this.session.registerTool({ toolName: 'state_capital' }, new StateCapitalTool(this)); + } +} diff --git a/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts b/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts new file mode 100644 index 00000000000..3f2da2be301 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { zod } from '@rushstack/mcp-server'; +import type { IRushMcpTool, RushMcpPluginSession, CallToolResult } from '@rushstack/mcp-server'; + +import type { ExamplePlugin } from './ExamplePlugin'; + +export class StateCapitalTool implements IRushMcpTool { + // eslint-disable-next-line @typescript-eslint/typedef + public static readonly SCHEMA = zod.object({ + state: zod.string().describe('The name of the state, in all lowercase') + }); + + public readonly plugin: ExamplePlugin; + public readonly session: RushMcpPluginSession; + + public constructor(plugin: ExamplePlugin) { + this.plugin = plugin; + this.session = plugin.session; + } + + // Getter: executes after constructor + public get schema(): typeof StateCapitalTool.SCHEMA { + return StateCapitalTool.SCHEMA; + } + + public async executeAsync(input: zod.infer): Promise { + const capital: string | undefined = this.plugin.configFile?.capitalsByState[input.state]; + + return { + content: [ + { + type: 'text', + text: capital + ? `The capital of "${input.state}" is "${capital}"` + : `Unable to determine the answer from the data set.` + } + ] + }; + } +} diff --git a/build-tests/rush-mcp-example-plugin/src/index.ts b/build-tests/rush-mcp-example-plugin/src/index.ts index 54f65999418..8866a23e566 100644 --- a/build-tests/rush-mcp-example-plugin/src/index.ts +++ b/build-tests/rush-mcp-example-plugin/src/index.ts @@ -1,4 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -export class X {} +import type { RushMcpPluginSession, RushMcpPluginFactory } from '@rushstack/mcp-server'; +import { ExamplePlugin, type IExamplePluginConfigFile } from './ExamplePlugin'; + +function createPlugin( + session: RushMcpPluginSession, + configFile: IExamplePluginConfigFile | undefined +): ExamplePlugin { + return new ExamplePlugin(session, configFile); +} + +export default createPlugin satisfies RushMcpPluginFactory; diff --git a/build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json b/build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json new file mode 100644 index 00000000000..534b3291d31 --- /dev/null +++ b/build-tests/rush-mcp-example-plugin/src/rush-mcp-example-plugin.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "State Capital Map", + "type": "object", + "required": ["capitalsByState"], + "properties": { + "$schema": { + "type": "string" + }, + "capitalsByState": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "A mapping of US state names (lowercase) to their capital cities." + } + }, + "additionalProperties": false +} diff --git a/common/reviews/api/mcp-server.api.md b/common/reviews/api/mcp-server.api.md new file mode 100644 index 00000000000..7c5b782d186 --- /dev/null +++ b/common/reviews/api/mcp-server.api.md @@ -0,0 +1,48 @@ +## API Report File for "@rushstack/mcp-server" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types'; +import * as zod from 'zod'; + +// @public (undocumented) +export type CallToolResult = zod.infer; + +export { CallToolResultSchema } + +// @public +export interface IRegisterToolOptions { + // (undocumented) + description?: string; + // (undocumented) + toolName: string; +} + +// @public +export interface IRushMcpPlugin { + // (undocumented) + onInitializeAsync(): Promise; +} + +// @public +export interface IRushMcpTool { + // (undocumented) + executeAsync(input: zod.infer): Promise; + // (undocumented) + readonly schema: TSchema; +} + +// @public +export type RushMcpPluginFactory = (session: RushMcpPluginSession, configFile: TConfigFile | undefined) => IRushMcpPlugin; + +// @public +export class RushMcpPluginSession { + // (undocumented) + registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; +} + +export { zod } + +``` From bde87b9623b3afd081d2f47d8f89ec0ab36e3c2e Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Wed, 28 May 2025 21:31:31 -0700 Subject: [PATCH 3/8] Finish decoupling "zod" so that rush-mcp-example-plugin does not need a package.json dependency for "zod" or "@rushstack/mcp-server" --- .../pluginFramework/RushMcpPluginSession.ts | 3 +++ .../src/pluginFramework/zodTypes.ts | 4 ++-- .../src/StateCapitalTool.ts | 23 +++++++++---------- common/reviews/api/mcp-server.api.md | 12 ++++++---- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts index 567f7ef007f..d2fcdf7f212 100644 --- a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts @@ -2,6 +2,8 @@ // See LICENSE in the project root for license information. import type { IRushMcpTool } from './IRushMcpTool'; +import * as zod from 'zod'; +import type { zodModule } from './zodTypes'; /** * Each plugin gets its own session. @@ -19,5 +21,6 @@ export interface IRegisterToolOptions { * @public */ export class RushMcpPluginSession { + public readonly zod: typeof zodModule = zod; public registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void {} } diff --git a/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts b/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts index a8f7aa6d92f..19bf22a2f12 100644 --- a/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts +++ b/apps/rush-mcp-server/src/pluginFramework/zodTypes.ts @@ -1,8 +1,8 @@ // 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'; -export { zod }; +import type * as zod from 'zod'; +export type { zod as zodModule }; import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types'; diff --git a/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts b/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts index 3f2da2be301..f6ad8ce6812 100644 --- a/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts +++ b/build-tests/rush-mcp-example-plugin/src/StateCapitalTool.ts @@ -1,17 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { zod } from '@rushstack/mcp-server'; -import type { IRushMcpTool, RushMcpPluginSession, CallToolResult } from '@rushstack/mcp-server'; +import type { IRushMcpTool, RushMcpPluginSession, CallToolResult, zodModule } from '@rushstack/mcp-server'; import type { ExamplePlugin } from './ExamplePlugin'; -export class StateCapitalTool implements IRushMcpTool { - // eslint-disable-next-line @typescript-eslint/typedef - public static readonly SCHEMA = zod.object({ - state: zod.string().describe('The name of the state, in all lowercase') - }); - +export class StateCapitalTool implements IRushMcpTool { public readonly plugin: ExamplePlugin; public readonly session: RushMcpPluginSession; @@ -20,12 +14,17 @@ export class StateCapitalTool implements IRushMcpTool { this.session = plugin.session; } - // Getter: executes after constructor - public get schema(): typeof StateCapitalTool.SCHEMA { - return StateCapitalTool.SCHEMA; + // ZOD relies on type inference generate a messy expression in the .d.ts file + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + public get schema() { + const zod: typeof zodModule = this.session.zod; + + return zod.object({ + state: zod.string().describe('The name of the state, in all lowercase') + }); } - public async executeAsync(input: zod.infer): Promise { + public async executeAsync(input: zodModule.infer): Promise { const capital: string | undefined = this.plugin.configFile?.capitalsByState[input.state]; return { diff --git a/common/reviews/api/mcp-server.api.md b/common/reviews/api/mcp-server.api.md index 7c5b782d186..ab3adecdbfc 100644 --- a/common/reviews/api/mcp-server.api.md +++ b/common/reviews/api/mcp-server.api.md @@ -5,10 +5,10 @@ ```ts import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types'; -import * as zod from 'zod'; +import type * as zodModule from 'zod'; // @public (undocumented) -export type CallToolResult = zod.infer; +export type CallToolResult = zodModule.infer; export { CallToolResultSchema } @@ -27,9 +27,9 @@ export interface IRushMcpPlugin { } // @public -export interface IRushMcpTool { +export interface IRushMcpTool { // (undocumented) - executeAsync(input: zod.infer): Promise; + executeAsync(input: zodModule.infer): Promise; // (undocumented) readonly schema: TSchema; } @@ -41,8 +41,10 @@ export type RushMcpPluginFactory = (session: RushMcpPluginSess export class RushMcpPluginSession { // (undocumented) registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; + // (undocumented) + readonly zod: typeof zodModule; } -export { zod } +export { zodModule } ``` From ea42fc655f836507c37ad4c944236d6be132059e Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Wed, 28 May 2025 23:12:13 -0700 Subject: [PATCH 4/8] Initial sketch of RushMcpPluginLoader --- apps/rush-mcp-server/src/index.ts | 2 +- .../pluginFramework/RushMcpPluginLoader.ts | 170 ++++++++++++++++++ .../pluginFramework/RushMcpPluginSession.ts | 14 +- .../src/schemas/rush-mcp-plugin.schema.json | 21 +++ .../src/schemas/rush-mcp.schema.json | 28 +++ apps/rush-mcp-server/src/server.ts | 7 + apps/rush-mcp-server/src/start.ts | 1 + .../rush-mcp-example-plugin/.npmignore | 3 + common/reviews/api/mcp-server.api.md | 4 +- 9 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts create mode 100644 apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json create mode 100644 apps/rush-mcp-server/src/schemas/rush-mcp.schema.json diff --git a/apps/rush-mcp-server/src/index.ts b/apps/rush-mcp-server/src/index.ts index 8e16a12951e..20b52112e78 100644 --- a/apps/rush-mcp-server/src/index.ts +++ b/apps/rush-mcp-server/src/index.ts @@ -8,5 +8,5 @@ export * from './pluginFramework/IRushMcpPlugin'; export * from './pluginFramework/IRushMcpTool'; -export * from './pluginFramework/RushMcpPluginSession'; +export { type IRegisterToolOptions, RushMcpPluginSession } from './pluginFramework/RushMcpPluginSession'; export * from './pluginFramework/zodTypes'; diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts new file mode 100644 index 00000000000..24c7466151f --- /dev/null +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts @@ -0,0 +1,170 @@ +// 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 `/common/config/rush-mcp/.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 { + 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 { + 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() + ); + } + } + } +} diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts index d2fcdf7f212..8353d1d3e1e 100644 --- a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts @@ -20,7 +20,17 @@ export interface IRegisterToolOptions { * * @public */ -export class RushMcpPluginSession { +export abstract class RushMcpPluginSession { public readonly zod: typeof zodModule = zod; - public registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void {} + public abstract registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; +} + +export class RushMcpPluginSessionInternal extends RushMcpPluginSession { + public constructor() { + super(); + } + + public override registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void { + // TODO: Register the tool + } } diff --git a/apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json b/apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json new file mode 100644 index 00000000000..e8e8757d9f1 --- /dev/null +++ b/apps/rush-mcp-server/src/schemas/rush-mcp-plugin.schema.json @@ -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 `/common/config/rush-mcp/.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 +} diff --git a/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json b/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json new file mode 100644 index 00000000000..0dd9a9546fe --- /dev/null +++ b/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "rush-mcp.json Configuration Schema", + "type": "object", + "properties": { + "mcpPlugins": { + "type": "array", + "description": "The list of plugins that `@rushstack/mcp-server` should load when processing this monorepo.", + "items": { + "type": "object", + "properties": { + "packageName": { + "type": "string", + "description": "The name of an NPM package that appears in the package.json \"dependencies\" for the autoinstaller." + }, + "autoinstaller": { + "type": "string", + "description": "The name of a Rush autoinstaller with this package as its dependency." + } + }, + "required": ["packageName", "autoinstaller"], + "additionalProperties": false + } + } + }, + "required": ["mcpPlugins"], + "additionalProperties": false +} diff --git a/apps/rush-mcp-server/src/server.ts b/apps/rush-mcp-server/src/server.ts index 9e4b7e0ced3..cd1d8d7d607 100644 --- a/apps/rush-mcp-server/src/server.ts +++ b/apps/rush-mcp-server/src/server.ts @@ -11,10 +11,12 @@ import { RushProjectDetailsTool, RushDocsTool } from './tools'; +import { RushMcpPluginLoader } from './pluginFramework/RushMcpPluginLoader'; export class RushMCPServer extends McpServer { private _rushWorkspacePath: string; private _tools: BaseTool[] = []; + private _pluginLoader: RushMcpPluginLoader; public constructor(rushWorkspacePath: string) { super({ @@ -23,9 +25,14 @@ export class RushMCPServer extends McpServer { }); this._rushWorkspacePath = rushWorkspacePath; + this._pluginLoader = new RushMcpPluginLoader(this._rushWorkspacePath); + } + public async startAsync(): Promise { this._initializeTools(); this._registerTools(); + + await this._pluginLoader.loadAsync(); } private _initializeTools(): void { diff --git a/apps/rush-mcp-server/src/start.ts b/apps/rush-mcp-server/src/start.ts index f131357c2b5..ac6441963b5 100644 --- a/apps/rush-mcp-server/src/start.ts +++ b/apps/rush-mcp-server/src/start.ts @@ -13,6 +13,7 @@ const main = async (): Promise => { } const server: RushMCPServer = new RushMCPServer(rushWorkspacePath); + await server.startAsync(); const transport: StdioServerTransport = new StdioServerTransport(); await server.connect(transport); diff --git a/build-tests/rush-mcp-example-plugin/.npmignore b/build-tests/rush-mcp-example-plugin/.npmignore index bc349f9a4be..dc4a664618b 100644 --- a/build-tests/rush-mcp-example-plugin/.npmignore +++ b/build-tests/rush-mcp-example-plugin/.npmignore @@ -30,3 +30,6 @@ # --------------------------------------------------------------------------- # DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. # --------------------------------------------------------------------------- + +!rush-mcp-plugin.json +!*.schema.json diff --git a/common/reviews/api/mcp-server.api.md b/common/reviews/api/mcp-server.api.md index ab3adecdbfc..cecf5940f2a 100644 --- a/common/reviews/api/mcp-server.api.md +++ b/common/reviews/api/mcp-server.api.md @@ -38,9 +38,9 @@ export interface IRushMcpTool = (session: RushMcpPluginSession, configFile: TConfigFile | undefined) => IRushMcpPlugin; // @public -export class RushMcpPluginSession { +export abstract class RushMcpPluginSession { // (undocumented) - registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; + abstract registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void; // (undocumented) readonly zod: typeof zodModule; } From fd8663859b03b6401a761bf7709041c5b461359f Mon Sep 17 00:00:00 2001 From: Pete Gonzalez <4673363+octogonz@users.noreply.github.com> Date: Wed, 28 May 2025 23:16:45 -0700 Subject: [PATCH 5/8] Add note --- apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts index 24c7466151f..88dec926982 100644 --- a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts @@ -153,6 +153,7 @@ export class RushMcpPluginLoader { 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()); From 3a2398db265675cb3b14cbbfaf500c57fb8e5f9d Mon Sep 17 00:00:00 2001 From: Lincoln <778157949@qq.com> Date: Mon, 2 Jun 2025 13:41:41 +0000 Subject: [PATCH 6/8] feat: resolve the todos --- .../src/pluginFramework/IRushMcpTool.ts | 4 ++- .../pluginFramework/RushMcpPluginLoader.ts | 36 +++++++++++++++---- .../pluginFramework/RushMcpPluginSession.ts | 20 +++++++++-- .../src/schemas/rush-mcp.schema.json | 4 +++ apps/rush-mcp-server/src/server.ts | 2 +- common/reviews/api/mcp-server.api.md | 2 +- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts b/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts index f6eaba91d70..46f325b0273 100644 --- a/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts +++ b/apps/rush-mcp-server/src/pluginFramework/IRushMcpTool.ts @@ -9,7 +9,9 @@ import type { CallToolResult } from './zodTypes'; * MCP plugins should implement this interface. * @public */ -export interface IRushMcpTool { +export interface IRushMcpTool< + TSchema extends zod.ZodObject = zod.ZodObject +> { readonly schema: TSchema; executeAsync(input: zod.infer): Promise; } diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts index 88dec926982..58fc212603f 100644 --- a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginLoader.ts @@ -2,10 +2,11 @@ // 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 { 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'; @@ -38,6 +39,11 @@ export interface IJsonRushMcpPlugin { * @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; } /** @@ -72,9 +78,11 @@ export class RushMcpPluginLoader { JsonSchema.fromLoadedObject(rushMcpPluginSchemaObject); private readonly _rushWorkspacePath: string; + private readonly _mcpServer: McpServer; - public constructor(rushWorkspacePath: string) { + public constructor(rushWorkspacePath: string, mcpServer: McpServer) { this._rushWorkspacePath = rushWorkspacePath; + this._mcpServer = mcpServer; } public async loadAsync(): Promise { @@ -84,7 +92,6 @@ export class RushMcpPluginLoader { ); if (!(await FileSystem.existsAsync(rushMcpFilePath))) { - // Should we report an error here? return; } @@ -135,7 +142,23 @@ export class RushMcpPluginLoader { RushMcpPluginLoader._rushMcpPluginSchemaObject ); - // TODO: Load and validate config file if defined by the manifest + 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; @@ -149,12 +172,11 @@ export class RushMcpPluginLoader { throw new Error(`Unable to load plugin entry point at ${fullEntryPointPath}: ` + e.toString()); } - const session: RushMcpPluginSessionInternal = new RushMcpPluginSessionInternal(); + const session: RushMcpPluginSessionInternal = new RushMcpPluginSessionInternal(this._mcpServer); let plugin: IRushMcpPlugin; try { - // TODO: Replace "{}" with the plugin's parsed config file JSON - plugin = pluginFactory(session, {}); + plugin = pluginFactory(session, rushMcpPluginOptions); } catch (e) { throw new Error(`Error invoking entry point for plugin ${jsonManifest.pluginName}:` + e.toString()); } diff --git a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts index 8353d1d3e1e..aa46003a8cf 100644 --- a/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts +++ b/apps/rush-mcp-server/src/pluginFramework/RushMcpPluginSession.ts @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { IRushMcpTool } from './IRushMcpTool'; import * as zod from 'zod'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; + +import type { IRushMcpTool } from './IRushMcpTool'; import type { zodModule } from './zodTypes'; /** @@ -26,11 +28,23 @@ export abstract class RushMcpPluginSession { } export class RushMcpPluginSessionInternal extends RushMcpPluginSession { - public constructor() { + private readonly _mcpServer: McpServer; + + public constructor(mcpServer: McpServer) { super(); + this._mcpServer = mcpServer; } public override registerTool(options: IRegisterToolOptions, tool: IRushMcpTool): void { - // TODO: Register the tool + 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)); + } } } diff --git a/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json b/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json index 0dd9a9546fe..14904b86cc3 100644 --- a/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json +++ b/apps/rush-mcp-server/src/schemas/rush-mcp.schema.json @@ -16,6 +16,10 @@ "autoinstaller": { "type": "string", "description": "The name of a Rush autoinstaller with this package as its dependency." + }, + "pluginName": { + "type": "string", + "description": "The name of the plugin. This is used to identify the plugin in the MCP server." } }, "required": ["packageName", "autoinstaller"], diff --git a/apps/rush-mcp-server/src/server.ts b/apps/rush-mcp-server/src/server.ts index cd1d8d7d607..b9a690f27f6 100644 --- a/apps/rush-mcp-server/src/server.ts +++ b/apps/rush-mcp-server/src/server.ts @@ -25,7 +25,7 @@ export class RushMCPServer extends McpServer { }); this._rushWorkspacePath = rushWorkspacePath; - this._pluginLoader = new RushMcpPluginLoader(this._rushWorkspacePath); + this._pluginLoader = new RushMcpPluginLoader(this._rushWorkspacePath, this); } public async startAsync(): Promise { diff --git a/common/reviews/api/mcp-server.api.md b/common/reviews/api/mcp-server.api.md index cecf5940f2a..5903120d31a 100644 --- a/common/reviews/api/mcp-server.api.md +++ b/common/reviews/api/mcp-server.api.md @@ -27,7 +27,7 @@ export interface IRushMcpPlugin { } // @public -export interface IRushMcpTool { +export interface IRushMcpTool = zodModule.ZodObject> { // (undocumented) executeAsync(input: zodModule.infer): Promise; // (undocumented) From 070718114afbd0362a028b724271c024d76f08a8 Mon Sep 17 00:00:00 2001 From: Lincoln <778157949@qq.com> Date: Mon, 2 Jun 2025 16:10:59 +0000 Subject: [PATCH 7/8] rush change --- .../octogonz-mcp-plugins_2025-06-02-16-10.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/mcp-server/octogonz-mcp-plugins_2025-06-02-16-10.json diff --git a/common/changes/@rushstack/mcp-server/octogonz-mcp-plugins_2025-06-02-16-10.json b/common/changes/@rushstack/mcp-server/octogonz-mcp-plugins_2025-06-02-16-10.json new file mode 100644 index 00000000000..4e9be68d75a --- /dev/null +++ b/common/changes/@rushstack/mcp-server/octogonz-mcp-plugins_2025-06-02-16-10.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/mcp-server", + "comment": "Introduce a plugin system for custom MCP tools", + "type": "minor" + } + ], + "packageName": "@rushstack/mcp-server" +} \ No newline at end of file From be1dac1ec3fd690a74d4665f52bd68798a5b4c48 Mon Sep 17 00:00:00 2001 From: Lincoln <778157949@qq.com> Date: Mon, 2 Jun 2025 16:30:48 +0000 Subject: [PATCH 8/8] fix ci issue --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a506c6af0e1..bd0c6aaa51b 100644 --- a/README.md +++ b/README.md @@ -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 |